Skip to content

Adding a Gradient Background to HorizonPlots #155

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

Open
wants to merge 13 commits into
base: develop
Choose a base branch
from

Conversation

Niphredil123
Copy link
Contributor

Inspired by this Horizon Plot task in the Notion roadmap, I have worked to add the ability to add a gradient background to horizon plots.

Key Actions

  • Added the function _apply_gradient_background() to the HorizonPlot class in horizon.py.
    • Takes the bounds of the exisiting GeoAxes.
    • Creates a set of background axes.
    • Creates a colourmap gradient based on the provided list.
    • Adds the gradient to the background axes using imshow().
    • Ensures the zorder of the starplot figure's GeoAxes is above the background axes.
  • Added an if loop to _init_plot() in the HorizonPlot class such that _apply_gradient_background() is applied if a gradient_preset parameter is provided when HorizonPlot is instantiated.
  • Added a file, styles/ext/gradient_presets.yml, containing six pre-set gradient colour gradient lists.
  • Added the extension GRADIENTS to styles/extensions.py so presets can be set using extensions.GRADIENTS["<preset name>"].

Additional Actions

  • Added a HorizonPlot example file. examples/horizon_gradient.py containing an example horizon plot with a gradient.
  • Added docs/examples/horizon-gradient.md to list the example horizon gradient plot in horizon_gradient.py.
  • Added an example card of a horizon gradient plot to docs/examples.md.

Adding a Gradient to a HorizonPlot

To create a HorizonPlot with a gradient, one includes the parameter gradient_preset when instansiating HorizonPlot. This can be done in two ways:

p = HorizonPlot(
    altitude=(0, 60),
    azimuth=(135, 225),
    lat=55.079112,  # Stonehaugh, England
    lon=-2.327469,
    dt=dt,
    style=style,
    resolution=4000,
    scale=0.9,
    gradient_preset=extensions.GRADIENTS["<preset name>"]    # presets = bold_sunset, civil_twilight, nautical_twilight, astronomical_twilight, true_night, pre_dawn
)

OR

p = HorizonPlot(
    altitude=(0, 60),
    azimuth=(135, 225),
    lat=55.079112,  # Stonehaugh, England
    lon=-2.327469,
    dt=dt,
    style=style,
    resolution=4000,
    scale=0.9,
    gradient_preset=[[0.0, '#0B0C40'], [0.3, '#191970'], [0.6, '#4169E1'], [0.8, '#87CEEB'], [1.0, '#F0F8FF']]
)

When adding the parameter as a list, the position values must range from 0-1.

Note that backgound_color must be set as transparent using a hex with alpha string (e.g. #ffffff00) when extending the PlotStyle(). This is because the gradient is plotted under the starplot plot, so will not be visible if the background is not transparent.

Images

Below are the images I make reference to in docs/examples/horizon-gradient.md (first two) and docs/example.md (last one).

horizon_gradient gradient_preset_examples horizon_gradient-sm

…lours. Colour presets can be called from the yaml file with extensions.GRADIENTS[<preset name>].
… This adds a vertical colour gradient to the background of the plot. e.g. to simulate twilight lighting in the sky.

The function takes the bounds of the figure's axes, creates a set of background axes, creates a colourmap and adds it to the background axes using imshow().

Added an if loop to _init_plot() so that _apply_gradient_background() is applied if a color_stop list is provided when HorizonPlot is instantiated.
There are six presets in total. Each preset contains a range of colours and each colour is listed with its position in the gradient.
…a horizon map looking south from Stonehaugh, England.
… file and moves it to the horizon-gradient.md file.
@steveberardi
Copy link
Owner

steveberardi commented Jul 27, 2025

@Niphredil123 Amazing, thanks so much for this contribution!! I've been wanting to add this to Starplot for awhile now, and your example image looks great! And I love that you added those six different gradient definitions.

Give me a few days to review this a little (I'm out of town right now), but one thing I'm curious about is if you got those gradients from some known source? Just wanna make sure we can include them as part of the starplot code and if they're from some open source project I'd like to add some reference to it.

Oh, also, I noticed you linked to the Notion roadmap, but we've moved over to Trello because they have a nicer free plan. I thought I updated all the docs to Trello, but maybe I missed a spot? Wondering where you got the notion link? Was it a previous version's docs?

And, thanks for following the contributor's guide and merging to the develop branch -- I'll have to update that branch with main cause it's really out of date and that's why this PR shows so many changes in the diff.

Thanks again! :)

Steve

@Niphredil123
Copy link
Contributor Author

@steveberardi I'm glad, you like it! It was a fun challenge to figure out. No rush with the review :)

For the colours, I looked at night sky colour palettes on Google images for inspiration and then used MakeGradients.app to design my own. That is also what I used to generated the little example squares. From their FAQs: "All gradients you create can be freely used in personal or commercial projects with no restrictions or attributions needed."

The only palette I actually used any hex codes from was number 19 from this Creative Booster inspiration post. I used their hex codes for magenta (#932885) and orange (#FFAF36) in bold_sunset and orange in nautical_twilight. The post is written with the intent to provide inspiration for sunset colours and I didn't use a full palette, but I am happy to change the hex codes if you would prefer. Anything else is a coincidence.

I found the Notion board in the contributing doc. I did find the Trello board later, but referenced the Notion board becuase that was where the gradients were specifically mentioned.

Hope it all runs smoothly when you check it :)

@steveberardi
Copy link
Owner

I finally had a chance to start looking at the code here, and try running it, and noticed a few things so far:

  • The gradient background adds a significant amount of time to creating the plot (2+ seconds on my machine). I think the biggest contributing factor to this is the resolution specified here. If I divide that number by 2, it runs about 2x faster and the result looks almost just as good 😀 I’m also wondering if we’d get some speedup by rendering the gradient in Pillow vs Matplotlib, or if there's some other way to speed it up
  • Ideally, I think it’d be nice to make the gradient definitions part of the existing background_color style property. In other words, make that property accept either a color string OR a list of tuples for the gradient. This would be similar to the line_style property (see code here). Then, in the _init_plot function if the background color is a list of tuples it would draw the gradient
  • It would also be awesome to make zenith and optic plots support these gradients, but instead of a vertical gradient for those make it radial from the center

Would you be interested in helping explore solutions to any of the above? If not, no worries, I can also merge this PR as-is and work on them.

Also, thanks so much for taking the time to read the Starplot code and following so many of the current patterns in the code! 😃

…_background(). This improves gradient smoothness.

Minimised the size of the gradient array in the same function to improve efficiency.
@Niphredil123
Copy link
Contributor Author

I'd be delighted to help explore solutions to the points you've listed, and anything else that comes up.

  • I had a look at the resolution, and since there really wasn't a difference when I changed the resolution, I realised I'd made an incorrect assumption that it was the array size that improved the smoothness of the gradient. I re-read the Docs for LinearSegmentedColourmap.from_list() and realised I'd missed N, which lets one change the RGB quantisation level.
    • I've pushed a change that minimises the gradient array and increases N.
    • Testing it on my machine, using np.array([[0], [1]]) for the gradient and N=750 for the RGB quantisation) produces a smoother gradient and is almost 2 seconds faster than the original method I used.
    • I played around with the N-values a bit and found that 750 was a good balance of smoothness and shaved off miliseconds, but feel free to experiment more.
    • That said, it is still 2 seconds slower than without a gradient, so there is definitely room look at other options like pillow.

Your other points sounds really good and I'd be happy to work towards them after invesitgating the code segements a bit more.
Let me know if I miss any code patterns you'd prefer I use :)

@steveberardi
Copy link
Owner

@Niphredil123 Great! One thing that might help with comparing runtimes of different gradient rendering methods is enabling "debug" mode on the plots. If you pass debug=True when creating the plot instance, then it'll log various debugging info (including runtimes of a few key functions). It seems the gradient stuff mostly affects the export of the plot, so keep an eye on the runtime of that function.

You can also add another function to that debug output by using the profile decorator.

One last thing that would be great to add for this is a "hash check". More info here, but let me know if any of that is unclear. The hash check stuff might have some difficulty with the gradients, so if you add a check and it's flaky (passes sometimes, fails other times) then one thing that might help avoid that is dramatically simplifying the gradient map (e.g. just one stop at 50% instead of 10 stops).

Thanks!! I think starplot users are gonna love this feature!

@steveberardi
Copy link
Owner

also, I totally forgot to mention a make command that might help with investigating the bottlenecks of the gradient rendering: if you're using docker and you enter make profile (see here) it'll run scripts/scratchpad.py in the docker container through Python's built-in profiler and then start a snakeviz webserver locally to let you explore the call stack and see runtimes of everything. I use this a lot when trying to optimize various things in Starplot.

- Used pcolormesh instead of imshow  to plot the gradient as this drastically improved export time.
- Changed how set self.ax.zorder was set to improve export time.
- Discovered that changing the altitude caused unexpected axes mis-alignment so added an event driven function such that the gradient background axes now resize with the main axes when it is drawn.
…ection of using cmaps.

Changed shade of orange in nautical_twilight to make it softer.
…d to horizon_checks.py.

Added the hashes for the new image to the hashlock.yml file.
@Niphredil123
Copy link
Contributor Author

Niphredil123 commented Aug 11, 2025

Thanks for all your advice on investigating the runtimes. After some playing around I've found that switching to using pcolormesh() as opposed to imshow() in the _apply_gradient_background() function improves the export time. It is now comparable to the export and total time with no gradient on my machine.

I also realised that I'd missed the fact that changing the altitude created mis-alignment of the background_gradient axes and the main axes. To resolve this I added a sub-function that syncs the background axes to the main axes when the plot is finally drawn using canvas.mpl_connect from matplotlib.

I've added the hash check and run it a few times. So far it has passed every time so I am fairly confident that the gradient isn't causing an issue with the hashing.

I ended up softening the orange in nautical_twilight so the updated image of the examples is below.

image

I will continue to investigate making this part of background_color and adding a radial gradient to the other plot types.

@steveberardi
Copy link
Owner

switching to using pcolormesh() as opposed to imshow() in the _apply_gradient_background() function improves the export time. It is now comparable to the export and total time with no gradient on my machine.

awesome!! 🎉

I will continue to investigate making this part of background_color and adding a radial gradient to the other plot types.

Also awesome! I'm hoping to include this as a major feature of the 0.16 release 😃

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.

2 participants