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

Plotly: Enable specified contour values for ranges; warn otherwise #3757

Merged
merged 5 commits into from
Aug 25, 2021

Conversation

anowacki
Copy link
Contributor

This PR is meant to help address #3356. It only does partially.

Note that there are no tests yet, though things seem to be
working locally for me. Guidance on how to add tests would
be greatly appreciated.

Plotly does not support arbitrary contour values, but can plot
specific contours if they are an equally-spaced range. However,
the current behaviour is to simply error if a range or collection
of values are passed via the levels keyword, since the current
implementation (correctly) adds two to whatever is passed in

This commit implements the plotting of contours if the levels
keyword argument is passed an AbstractRange, or if a set
of arbitrary values are passed. In the latter case, however,
since this is not supported by Plotly, a range based on the
first and last values of the collection passed in is created
and used to define the contours. A warning is then issued to
the user.

Otherwise, any other types are assumed to be number-like and
adjusted as previously. Note that Plotly does not guarantee
the exact number of contours will be used.

See plotly/plotly.js#4503 (Plotly issue
tracking the ability to set arbitrary contours).

Possible things to look at are:

  • How we warn the user. I couldn't find any mechanism in to which
    to hook to do this properly, since this is a partial limitation
    of a backend, not a complete absence of a feature.
  • Whether changing a user's set of contour values for them is the
    right thing to do, even with a warning, or we should instead
    error when passed a tuple or vector. Or should we check that
    the values therein are actually equally spaced, in which case
    we could plot those exact contours?

@anowacki
Copy link
Contributor Author

With this PR:

x = y = -π:0.01:π
z = 1.01.*sin.(x).*cos.(y)'
levels = -1:0.5:1
plot(
    [contour(x, y, z, levels=l, c=:rainbow)
     for l in (length(levels), levels, collect(levels))]...,
    grid=false
)

newplot

@codecov
Copy link

codecov bot commented Aug 19, 2021

Codecov Report

Merging #3757 (0b800ac) into master (214663a) will increase coverage by 0.09%.
The diff coverage is 100.00%.

Impacted file tree graph

@@            Coverage Diff             @@
##           master    #3757      +/-   ##
==========================================
+ Coverage   63.05%   63.14%   +0.09%     
==========================================
  Files          28       28              
  Lines        7064     7076      +12     
==========================================
+ Hits         4454     4468      +14     
+ Misses       2610     2608       -2     
Impacted Files Coverage Δ
src/args.jl 73.40% <100.00%> (+0.29%) ⬆️
src/backends/unicodeplots.jl 71.42% <0.00%> (-1.18%) ⬇️
src/backends/gr.jl 89.21% <0.00%> (-0.07%) ⬇️
src/backends.jl 64.60% <0.00%> (+4.07%) ⬆️

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 214663a...0b800ac. Read the comment docs.

@t-bltg
Copy link
Member

t-bltg commented Aug 19, 2021

Note that there are no tests yet, though things seem to be
working locally for me. Guidance on how to add tests would
be greatly appreciated.

The are no image comparison test for backends other than GR actually.

But you can try running and compare the examples against the docs:

using Plots; plotlyjs()
for i  1:length(Plots._examples)
    i  Plots._backend_skips[:plotlyjs] && continue
    try
        Plots.reset_defaults()
        map(x->Base.eval(Main, x), Plots._examples[i].exprs)
        png("plotly-$i")
    catch err
        @error "" bt=(err, stacktrace(catch_backtrace()))
    end
end

@BeastyBlacksmith
Copy link
Member

BeastyBlacksmith commented Aug 19, 2021

I think we got 3 different types of tests:

For the given case here I'd recommend to add some of the latter

@t-bltg t-bltg added the Plotly label Aug 20, 2021
@anowacki
Copy link
Contributor Author

Thanks for the explanations, both.

But you can try running and compare the examples against the docs:

Okay—I have done this and everything looks the same as in the docs. I guess this is expected because the only change is to the contour handling for Plotly, and the example does not specify any contour number or levels.

The caveat to this is that examples 32, 47 and 50 produce empty files and the kaleido plotly command spawned by Julia hangs for these examples until I kill it. This happens on my machine without this PR as well, so I don't think it can be related.

Frontend tests

I'm not totally sure how these tests work, but I guess I should add a file test/test_contours.jl which just checks that a backend gets the value passed in via the levels keyword argument?

Backend tests

I can't see any backend tests for Plotly at the moment. Should I add a test/test_plotly.jl file and add tests for the changes there? I'm afraid I don't have the time to add tests for other functionality.

@anowacki
Copy link
Contributor Author

In starting to write these tests, I realise I don't understand what the levels keyword argument is actually meant to do when it gets a NTuple{2, Integer}. The docs say:

levels | levs, nlev, nlevels | 15 | Integer, NTuple{2,Integer}, or AbstractVector | Levels or number of levels (or x-levels/y-levels) for a contour type.

What are the number of x-levels and y-levels for a contour plot? I don't understand what that could mean here.

Trying the example above with contours(x, y, z, levels=(3, 5)) with GR just throws a MethodError in convert at GR:src/GR.jl:2662 (without this PR and with). Clearly GR is trying to convert the tuple into a set of contours, which is not intended per the docs. However I think this is a separate issue.

Just asking since I will need to update this PR to reflect.

@t-bltg
Copy link
Member

t-bltg commented Aug 20, 2021

What are the number of x-levels and y-levels for a contour plot? I don't understand what that could mean here.

Using git blame, traces appear in cf4fcf3, #99 and #101, although I did not find explicit reference to Tuples apart from arg_desc.jl.
I'm guessing that it can be removed from the docs ? (needs confirmation though).

Should I add a test/test_plotly.jl

My advice on this is to add a minimal test_plotly.jl if the test is representative and robust (i.e. not subject to recurrent changes in plotly).

@BeastyBlacksmith
Copy link
Member

BeastyBlacksmith commented Aug 20, 2021

Frontend tests

I'm not totally sure how these tests work, but I guess I should add a file test/test_contours.jl which just checks that a backend gets the value passed in via the levels keyword argument?

If there is no processing of the argument on the plots-side than adding a test is not really needed, but wouldn't hurt either.

Those tests really check if the Plot object fields have the expected values.

Backend tests

I can't see any backend tests for Plotly at the moment. Should I add a test/test_plotly.jl file and add tests for the changes there? I'm afraid I don't have the time to add tests for other functionality.

Thats correct and also fine to only test what is done in this PR. Hopefully it will grow over time.

Thanks a lot for your effort!

@BeastyBlacksmith
Copy link
Member

What are the number of x-levels and y-levels for a contour plot? I don't understand what that could mean here.

I think we can remove that if its not supported by any backend

Plotly does not support arbitrary contour values, but can plot
specific contours if they are an equally-spaced range.  This
commit implements the plotting of contours if the `levels`
keyword argument is passed an `AbstractRange`, or if a set
of arbitrary values are passed.  In the latter case, however,
since this is not supported by Plotly, a range based on the
first and last values of the collection passed in is created
and used to define the contours.  A warning is then issued to
the user.

Otherwise, any other types are assumed to be number-like and
adjusted as previously.  Note that Plotly does not guarantee
the exact number of contours will be used.

This partly addresses JuliaPlots#3356.

See plotly/plotly.js#4503 (Plotly issue
tracking the ability to set arbitrary contours).
- Test that the `levels` keyword argument is correctly stored.
- For the Plotly (and PlotlyJS) backend, test that `levels`
  is correctly converted to backend-specific settings.
@anowacki
Copy link
Contributor Author

I have updated the PR with tests and corrected the logic in converting levels to Plotly-specific settings.

In looking over this I realised that the different backends do different things if the input to levels is not correct. As it stands, this PR and at least some other backends simply ignore arguments which are not a vector (isa AVec) or integer (isinteger()) and so the default number of contours are used. Should we instead throw an ArgumentError in general for unsupported arguments?

@t-bltg
Copy link
Member

t-bltg commented Aug 20, 2021

I have updated the PR with tests and corrected the logic in converting levels to Plotly-specific settings.

Thanks. Can you also update src/arg_desc.jl by removing the reference to NTuple{...} for levels ?

In looking over this I realised that the different backends do different things if the input to levels is not correct. As it stands, this PR and at least some other backends simply ignore arguments which are not a vector (isa AVec) or integer (isinteger()) and so the default number of contours are used. Should we instead throw an ArgumentError in general for unsupported arguments?

I'm supporting unification of the backends on this and error out on unsupported (not Integer nor AVec).

@anowacki
Copy link
Contributor Author

Can you also update src/arg_desc.jl by removing the reference to NTuple{...} for levels ?

No problem—I'll update shortly.

I'm supporting unification of the backends on this and error out on unsupported (not Integer nor AVec).

Sounds like a a good, user-friendly solution to me and I'm happy to add that here or as a separate PR if that's more appropriate. Where should the check go? I haven't found any sort of central argument-checking place in the codebase so far, but likely I have missed it. Or perhaps it should go in RecipesPipeline?

@t-bltg
Copy link
Member

t-bltg commented Aug 23, 2021

Sounds like a a good, user-friendly solution to me and I'm happy to add that here or as a separate PR if that's more appropriate. Where should the check go? I haven't found any sort of central argument-checking place in the codebase so far, but likely I have missed it. Or perhaps it should go in RecipesPipeline?

Thinking out loud, maybe in src/utils.jl something like:

function check_supported_levels(series)
   if !(typeof(series[:levels]) <: Union{Integer,Avec})
       error("Unsupported message ...")
   end
end

, and modify the backends accordingly.

@anowacki
Copy link
Contributor Author

maybe in src/utils.jl

I'd be happy to do that, but that suggestion does then still require quite a lot of code duplication in the backends—each one will separately need to call e.g. check_supported_levels.

As a potential alternative, it seems like RecipesPipeline.preprocess_attributes!(::KW) (as defined in Plots.jl) seems like a really good candidate for this. There are already checks for things like legend placement in convertLegendValue and label naming in label_to_string, both ultimately called from preprocess_attributes!.

How about calling the function you posted @t-bltg (or equivalent) in preprocess_attributes!(::KW)?

@t-bltg
Copy link
Member

t-bltg commented Aug 24, 2021

As a potential alternative, it seems like RecipesPipeline.preprocess_attributes!(::KW) (as defined in Plots.jl) seems like a really good candidate for this. There are already checks for things like legend placement in convertLegendValue and label naming in label_to_string, both ultimately called from preprocess_attributes!.

How about calling the function you posted @t-bltg (or equivalent) in preprocess_attributes!(::KW)?

Agreed, I haven't checked if all backends behave in the same way on keyword levels, but if checking valid kw upstream in the pipeline is feasible, then I'm all in !

The `levels` keyword argument does not actually support
values of type `NTuple{2,Integer}` despite the existing documentation,
so remove the mention of this from `_arg_desc` and update the
description of what `levels` means.
Checking of the `levels` keyword argument will be moved out
of individual backends' code, so we can assume that `levels`
if present in `plotattributes` is either an `AbstractVector` or
`Integer`.
This commit moves the check for the correctness of the `levels`
keyword argument into `RecipesPipeline.preprocess_attributes!(::KW)`
and calls the new function `check_contour_levels`, which in turn
will throw an `ArgumentError` if the argument is not correct.

To that end:
- Remove the check for non-integer and non-vector values of
  `levels` from the PyPlot backend code, and
- Add low-level tests for the new function and high-level tests
  for contour plots.
@anowacki
Copy link
Contributor Author

From a survey of the backends, only PyPlot previously checked what levels was, and so it was simple enough to remove a couple of lines of code there. Now anything backend-specific I think will have come after preprocess_attributes! and so can rely on levels being an Integer or AbstractVector.

@anowacki
Copy link
Contributor Author

Note that I added a check that levels is greater than 0 if an integer. GR, PyPlot and Plotly accepted 1 level but not 0 when I tested, so this seems like it would not break existing code that worked, but I guess it is technically a breaking change. I can't imagine many people's workflows relied on being able to pass negative numbers of contours.

@t-bltg
Copy link
Member

t-bltg commented Aug 25, 2021

I can't imagine many people's workflows relied on being able to pass negative numbers of contours.

Safe assumption, we'll see if any issue pops up on this matter.

@t-bltg t-bltg merged commit 52ec432 into JuliaPlots:master Aug 25, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants