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

Adding dotplots #1950

Open
sethaxen opened this issue May 16, 2022 · 9 comments
Open

Adding dotplots #1950

sethaxen opened this issue May 16, 2022 · 9 comments

Comments

@sethaxen
Copy link
Contributor

sethaxen commented May 16, 2022

I'd like to revive a subset of JuliaPlots/StatsMakie.jl#107 to add a dotplot function that constructs Wilkinson dot plots (a la ggplot2::geom_dotplot). This consists of

  1. a function for binning univariate data into stacks
  2. a stacks recipe for constructing stacks of markers, given the position of each stack in the data dimension, the number of markers in each stack, and the width of the stack in the data dimension
  3. a dotplot recipe that calls the function in (1) and passes the data to (2) for plotting

(1) and (3) are trivial. (2) is a little tricky because the stacks must satisfy 4 constraints:

  • The markers should not be squished when viewed by the user, e.g. a circular dot should be a circle.
  • The width of the markers in the data dimension should be fixed
  • In the non-data dimension, the markers should perfectly stack (i.e. their boundaries just touch).
  • When the user resizes the plot, the above 3 constraints should still hold

There are a few ways to do this, but they generally require some information about the axis (e.g. limits, aspect ratio). Can this information be accessed within a recipe?

@asinghvi17
Copy link
Member

In general, I would say yes.

The first constraint can be addressed by having marker sizes in pixelspace; markerspace = :pixel suffices for this.

I'm not clear on the second constraint, but you may want to dynamically update markersize depending on the plot's parent Scene's limits.
One can get the plot's parent Scene's pixel area and project that down into data space (the direct inversion of this function in GeoMakie), and so extract the limits of the axis in its transformed space. Then, just apply the inverse transform (usually inv_trans = Makie.inverse_transform(scene.transformation[].transform_func) and then apply_transform on that function) of the scene to obtain coordinates in "user input space".

For the third constraint, a similar consideration applies. Knowing the marker's position in "input space", or even the Scene's transformed space if you're so inclined, you can project that into pixel space. Knowing the marker size in pixel space means that you can just continuously add that to the y-axis. If you want to plot values directly in pixel space, simply use space=:pixel.

For the fourth constraint - again, just recalculate when the relevant aspects of the Scene (transformation, px_area) change.

@jkrumbiegel
Copy link
Member

Here's one example implementation. I think px_area you can get inside a plotting function, but not ax.finallimits. But there was a way to extract it from projection matrices I think, even if that's not the cleanest way. @ffreyer showed it to me somewhere but I forgot.

using Random
Random.seed!(123)

f = Figure()
ax = Axis(f[1, 1])

xs = 1:15
binwidth = step(xs)

ms = lift(ax.scene.px_area, ax.finallimits) do pxa, lims
    widths(pxa)[1] / widths(lims)[1] * binwidth
end

dots = lift(ax.scene.px_area, ax.finallimits) do pxa, lims
    counts = rand(1:7, length(xs))
    points = Point2f[]
    for (x, count) in zip(xs, counts)
        for i in 1:count
            push!(
                points,
                Point2f(
                    (x - lims.origin[1]) / (lims.widths[1]) * pxa.widths[1],
                    (i - 0.5) * ms[],
                )
            )
        end
    end
    points
end

scatter!(ax, dots, markersize = ms, space = :pixel)
xlims!(ax, xs.start - 1, xs.stop + 1)
hideydecorations!(ax)
# lines(f[1, 2], 1:10) # add other content to figure
f

grafik

@jkrumbiegel
Copy link
Member

In general these kinds of plot are always a bit brittle because if the aspect ratio changes too much the dots have to flow out of the scene.

@sethaxen
Copy link
Contributor Author

Thanks @jkrumbiegel and @asinghvi17 for the suggestions!

I think px_area you can get inside a plotting function, but not ax.finallimits. But there was a way to extract it from projection matrices I think, even if that's not the cleanest way. @ffreyer showed it to me somewhere but I forgot.

This seems to work:

    finallimits = lift(px_area, transformation(scene).transform_func) do pxa, tfunc
        orig = project(camera(scene), :pixel, :data, apply_transform(tfunc, zero(origin(pxa))))
        corn = project(camera(scene), :pixel, :data, apply_transform(tfunc, widths(pxa)))
        wid = corn - orig
        Rect2(Vec2(orig[1:2]), Vec2(wid[1:2]))
    end

In general these kinds of plot are always a bit brittle because if the aspect ratio changes too much the dots have to flow out of the scene.

Yes, that kind of brittleness is unavoidable.

@ffreyer
Copy link
Collaborator

ffreyer commented May 18, 2022

pv = scene.camera.projectionview transforms Point4f(x, y, z, 1) from data space to clip space (-1..1, -1..1, 0..1). For 2d scenes pv[1, 1], pv[2, 2] are the scaling and pv[1, 4], pv[2, 4] (or maybe pv[4, 1], pv[4, 2], I get this wrong sometimes) are the offsets of linear transformations.
So going from axis coordinates to clip space you get pv[1, 1] * x + pv[1, 4] where x = xmin (xmax) result in -1 (+1). You can get the axis limits by reversing this, i.e. xmin, xmax = ((-1, 1) .- pv[1, 4]) ./ pv[1, 1].

@sethaxen
Copy link
Contributor Author

Thanks, @ffreyer! This worked for getting data limits:

finallimits = lift(scene.camera.projectionview) do pv
    xmin, xmax = minmax((((-1, 1) .- pv[1, 4]) ./ pv[1, 1])...)
    ymin, ymax = minmax((((-1, 1) .- pv[2, 4]) ./ pv[2, 2])...)
    origin = Makie.Vec2(xmin, ymin)
    Makie.Rect2(origin, Makie.Vec2(xmax, ymax) - origin)
end
xlims!(ax, xs.start - 1, xs.stop + 1)
hideydecorations!(ax)

@jkrumbiegel (how) can this be done within a recipe (with no access to axis)?

@jkrumbiegel
Copy link
Member

jkrumbiegel commented May 19, 2022

It currently can't be done from within recipes, we need either axis hints in recipes or a step above the current pipeline that can control Axis / Legend etc.

Maybe you can overload how data limits are determined for this plot type so you get meaningful x limits.

@SimonDanisch
Copy link
Member

That's what Makie.parent_scene does..But I think access to the axis is needed here.

@asinghvi17
Copy link
Member

asinghvi17 commented Mar 10, 2024

This kind of plot would need to trigger the axis to reset limits after its calculation is done. Maybe we could place a hook in a Scene's attributes, that an Axis can pick up on when it's updated and react to it?

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

No branches or pull requests

5 participants