8000 [ENH]: Good looking date/time axes with primary and secondary annotation and interval annotation · Issue #27394 · matplotlib/matplotlib · GitHub
[go: up one dir, main page]

Skip to content

[ENH]: Good looking date/time axes with primary and secondary annotation and interval annotation #27394

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
eelcodoornbos opened this issue Nov 29, 2023 · 7 comments

Comments

@eelcodoornbos
Copy link

Problem

The default matplotlib date/time axes are difficult to customize to look professional. Date values usually denote time intervals (days, months, years), but matplotlib default date labels are attached to single ticks that mark the start of that interval. And since all date/time annotations are usually put on a single axis, the labels get long, and often have to be plotted at an angle, which does not look pleasing and makes the plots more difficult to read.

Proposed solution

Good-looking and easy to read date/time axes need implementation of two features:

  • Interval labels - where the labels for years, months, week numbers and days are drawn between two adjacent tick marks
  • Primary and secondary axes that are vertically stacked, so that the top ticks/labels can denote a smaller unit of time than the bottom one.

Some excellent examples of the look that can be achieved using these two features can be seen in the cookbook of the Generic Mapping Tools: https://docs.generic-mapping-tools.org/latest/cookbook/options.html#cartesian-time-axes

The "interval labels" feature could also be made reusable for custom interval annotations, like in the further example on that website: https://docs.generic-mapping-tools.org/latest/cookbook/options.html#custom-axes

@rcomer
Copy link
Member
rcomer commented Nov 29, 2023

Does ConciseDateConverter address your "primary and secondary axes" point?
https://matplotlib.org/stable/gallery/ticks/date_concise_formatter.html

@eelcodoornbos
Copy link
Author
eelcodoornbos commented Nov 29, 2023

No, ConciseDateConverter does not add a secondary axis, only a single additional label. I find the examples not very intuitive to read, as this additional label is not a commonly used convention. The reader first has to parse what it means. For the example where the time series ends on April 1, 2005, it gets particularly confusing, since none of the dates displayed on the axis actually fall inside that month.

The examples with the true primary and secondary axes in the Generic Mapping Tools documentation on the other hand, convey a much more conventional calendar-like approach to displaying dates and times, that should be clear to readers of the plots much more quickly.

Perhaps the functionality of ax.secondary_xaxis() could be reused for this in matplotlib, when the secondary axis is put on the same side as the primary and provided with an additional offset.

Another example implementation of these two features, with an automatic selection of the appropriate ticks and labels, is available in a web application I have built: https://spaceweather.knmi.nl/viewer/. Here, the interval annotation feature was achieved by simply adding an offset to the labels, relative to the ticks, and changing their alignment (from centred to left-aligned). Having a choice to center those interval annotation labels between two ticks would be even better.

@jklymak
Copy link
Member
jklymak commented Nov 29, 2023

We do not currently have a way to add an extra level of ticks.

You can fake this with a secondary x axis or with minor and major ticks on the main axis. If I were doing it, I'd place a secondary xaxis at y=0 and just add carriage returns to the tick labels rather than messing with offsets by hand.

Labelling time in general is quite hard and there will always be taste differences. If your taste differs from the available defaults, you are going to have to do some work.

The general request for multiple label levels is the same as #6321

@eelcodoornbos
Copy link
Author

Thanks for those considerations. The Stack Overflow example mentioned in #6321, already implements both dual labels and interval labelling with some additional code.
So then I think a good solution would be an alternative to date_concise_formatter.html, which implements these features "behind-the-scenes" to offer two levels of annotation, and adds some sensible defaults based on the plot width and time interval for casual users, as well as offers parameters to customize the primary and secondary date/time annotations.

I agree that labelling time is hard, and the above would take a considerable implementation and fine-tuning effort. But I don't think the main differences between the examples under the links I gave above on the one hand, and those in date_concise_formatter.html and the current matplotlib default on the other, come down purely to a matter of taste. I think the readability/usability differences can be considered objectively.

@story645
8000 Copy link
Member
story645 commented Nov 30, 2023

which implements these features "behind-the-scenes" to offer two levels of annotation, and adds some sensible defaults based on

Edit 2: If I follow correctly, the simplest version of what's being proposed here is

  • DateIntervalLocator: trick here is it doesn't draw ticks/is used for invisible positioning
  • DateIntervalFormatter: put this label at invisible ticks

Which I think is reasonable to implement at the major/minor axes - so no leveling - as a first pass.

Then, building around some of the ideas here, what about something that can compose the various Datetime Locators/Formatters <- which side note, this was really hard to find & I wonder if making it easier to find would improve things.

Something like this super explicit version as a first pass and then we can think about sensible defaults:

DateCompositeLocator([WeekdayLocator(**params),  MicrosecondLocator(params)] , separate_axes = False) 

DateCompositeFormatter(list of format strings matching composite locator)

Because here we know the hierarchy/it's defined by virtue of the units. The # of possible levels are fixed and we can start w/ no defaults and then grow out to defaults. This could also be a good starting point on which to build a more generalized solution later?

@eelcodoornbos
Copy link
Author
eelcodoornbos commented Nov 30, 2023

Yes, something like that would be a very good starting point.

The default locators/formatters for https://spaceweather.knmi.nl/viewer/ are implemented (in JS) here: https://gitlab.com/KNMI-OSS/spaceweather/knmi-hapi-timeline-viewer/-/blob/main/src/lib/svg_plots/plot_frame_components/datetimetickinfo.js
These were created by trial/error/tweaking, but it seems to scale well across a large range of plot canvas sizes and time ranges, from smartphones to large high-res displays and from centuries to seconds. Something like that could be translated to use the matplotlib locators/formatters, and would probably have to be tweaked a bit further to correct for font/plot size/unit differences between this JS/CSS/SVG implementation and matplotlib. Those could then feed into the DateCompositeLocator/Formatters.

@jklymak
Copy link
Member
jklymak commented Nov 30, 2023

The aesthetics of the date locators and formatters is a side issue to me - anyone can write their own to override the Matplotlib-offered ones, and the defaults aren't going to change until we do a style-break release.

The first job is deciding on an API for how to add a second (or more) level of tick annotations.

DateCompositeLocator([WeekdayLocator(**params), MicrosecondLocator(params)] , separate_axes = False)

This could also be a good starting point on which to build a more generalized solution later?

I think the form of the API would need to be figured out first. For instance, I'm not a fan of the Locator controlling if there are two axis levels or not. The locator is really just there to give tick values. Rather it seems an axis-level method eg xaxis.set_second_level_locator(BooLocator), or an axes-level method (ax.second_xaxis).

Overall, the problem of adding a second level of tick labels is largely an issue of how to place the tick labels in a way that looks good without manual tweaking. It's a tricky problem in general. There will also be the question of styling how the intervals in the second row are indicated.

I think the readability/usability differences can be considered objectively.

I'm sure it is possible to come up with metrics for such things, and design studies to measure them.

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

No branches or pull requests

4 participants
0