MakieOrg / Makie.jl

Interactive data visualizations and plotting in Julia
https://docs.makie.org/stable
MIT License
2.42k stars 313 forks source link

Enhance the `DateTime`, `Date` and `Time` axis formatting #4404

Open mathieu17g opened 1 month ago

mathieu17g commented 1 month ago

Feature description

Extend the handling of TimeType type for axis ticks and labels by handling Date and Time and enable the use of a formatter specified as a DateFormat or any Function on ticks' values

Here are three light modifications in src/date-integration.jl of v0.21.11, enabling those:

1. Add easy to define formatter with DateFormat

In src/date-integration.jl add the following get_ticklabels functions:

# Default implementation for `get_ticklabels` with `DateFormat`.
# Used before DateTimeConversion is set to the axis via plots addition.
get_ticklabels(::DateFormat, values) = get_ticklabels(Automatic(), values)

function get_ticklabels(::Type{T}, df::DateFormat, values) where {T<:TimeType}
    return [Dates.format(number_to_date(T, v), df) for v in values]
end

2. Extend get_ticks for DateTimeConversion to use PlotUtilsjl function optimize_datetime_ticks for Date beyond DateTime. And use the DateFormat formatter or any formatter defined by a Function on ticks' values.

Add to function get_ticks(conversion::DateTimeConversion, args...)

A conversion of extrema values to DateTime corresponding values

<<< if T <: DateTime
>>> if T <: Union{Date,DateTime}
        if T == Date
            # Convert T float extrema values to DateTime corresponding float values
            vmin = date_to_number(DateTime, DateTime(number_to_date(T, vmin)))
            vmax = date_to_number(DateTime, DateTime(number_to_date(T, vmax)))
        end

And at the end of the corresponding if clause, before returning, either convert back ticks values computed by optimize_datetime_ticks to the axis unit and use the DateFormat type formatter or use Function type formatter on internal numbers ticks' values:

<<< conversion, dates = PlotUtils.optimize_datetime_ticks(vmin, vmax; k_min=k_min, k_max=k_max)
    return conversion, dates
>>> tickvalues, ticklabels = PlotUtils.optimize_datetime_ticks(vmin, vmax; k_min=k_min, k_max=k_max)
    if T == Date
        tickvalues = date_to_number.((T,), T.(number_to_date.((DateTime,), tickvalues)))
    end
    ticklabels = if formatter isa Automatic
        ticklabels
    elseif formatter isa DateFormat
        get_ticklabels(T, formatter, tickvalues)
    elseif formatter isa Function
        formatter(tickvalues)
    else
        error("$(formatter) not supported for DateTimeConversion")
    end
    return tickvalues, ticklabels

3. Handle the Time type in get_ticks for DateTimeConversion by using the DateFormat formatter (with its restriction of not handling precision below the milisecond) or any formatter defined by a Function on ticks' values

<<< else
        # TODO implement proper ticks for Time Date
        tickvalues = get_tickvalues(formatter, scale, vmin, vmax)
        dates = number_to_date.(T, round.(Int64, tickvalues))
        return tickvalues, string.(dates)
    end
>>> elseif T == Time
        tickvalues = get_tickvalues(ticks, scale, vmin, vmax)   
        ticklabels = if formatter isa Automatic
            times = number_to_date.(T, round.(Int64, tickvalues))
            string.(times)
        elseif formatter isa DateFormat
            get_ticklabels(T, formatter, tickvalues)
        elseif formatter isa Function
            formatter(tickvalues)
        else
            error("$(formatter) not supported for DateTimeConversion")
        end
        return tickvalues, ticklabels
    else
        error("$(T) not supported for DateTimeConversion")
    end

Output plot examples

This should enable to produce this type of Date, DateTime and Time axis formatting with Automatic, DateFormat or Function formatter.

~By the way, it would be interesting to document a bit the number_to_date functions, even if not exported, with a docstring. It would ease the use of the Function formatted.~ I had not seen the docstrings, maybe a mention to it in documentation then

Examples code ```julia using GLMakie using Dates # Generate some sample data dates = [Date(2022, 1, 1) + Dates.Day(i) for i = 0:10] datetimes = [DateTime(2022, 1, 1, i, 20) for i = 0:12] times = [Time(0, 0, 0, i) for i = 0:10] values = [1, 3, 2, 4, 5, 3, 6, 4, 7, 6, 8] values4datetimes = [1, 3, 2, 4, 5, 3, 6, 4, 7, 6, 8, 7, 10] # Create a Figure fig = Figure(size = (1500, 1000)) # Add an Axis with and without Date formatting on X axis ax11 = Axis( fig[1, 1], xticks = WilkinsonTicks(4; k_max = 4), title = rich("Date format on X axis with ", rich("Automatic", color = :blue, font = "Consolas"), " as formatter"), ) ax21 = Axis( fig[2, 1], xticks = WilkinsonTicks(4; k_max = 4), xtickformat = dateformat"Y-u-dd (e)", title = rich( "Date format on X axis with ", rich("dateformat\"Y-u-dd (e)\"", color = :blue, font = "Consolas"), " as formatter", ), ) ax31 = Axis( fig[3, 1], xticks = WilkinsonTicks(10), xtickformat = ( V -> [ dayofweek(v) == 1 ? Dates.format(v, "e") * "\n" * Dates.format(v, "Y-W") * string(week(v); pad = 2) : Dates.format(v, "e") for v in Makie.number_to_date.((Date,), V) ] ), title = rich( "Date format on X axis with \n", rich( "V -> [dayofweek(v) == 1 ? Dates.format(v, \"e\") * \"\\n\" * \nDates.format(v, \"Y-W\") * string(week(v); pad = 2) : \nDates.format(v, \"e\") \nfor v in Makie.number_to_date.((Date,), V)]\n", color = :blue, font = "Consolas", ), " as formatter", ), ) # Create a line plots with Date type for x value lines!(ax11, dates, values) lines!(ax21, dates, values) lines!(ax31, dates, values) # Add an Axis with and without DateTime formatting on X axis ax12 = Axis( fig[1, 2], xticks = WilkinsonTicks(2; k_max = 2), title = rich("DateTime format on X axis with ", rich("Automatic", color = :blue, font = "Consolas"), " as formatter"), ) ax22 = Axis( fig[2, 2], xticks = WilkinsonTicks(4; k_max = 4), xtickformat = dateformat"u-dd Hh", title = rich( "DateTime format on X axis with ", rich("dateformat\"u-dd Hh\"", color = :blue, font = "Consolas"), " as formatter", ), ) ax32 = Axis( fig[3, 2], xticks = WilkinsonTicks(4; k_max = 4), xtickformat = ( V -> [ hour(v) == 0 ? string(hour(v)) * "h\n" * string(Date(v)) : string(hour(v)) * "h" for v in Makie.number_to_date.((DateTime,), V) ] ), title = rich( "DateTime format on X axis with function\n", rich( "V -> [hour(v) == 0 ? string(hour(v)) * \"h\\n\" * \nstring(Date(v)) : string(hour(v)) * \"h\" \nfor v in Makie.number_to_date.((DateTime,), V)]\n", color = :blue, align = :left, font = "Consolas", ), " as formatter", ), ) # Create a line plots with DateTime type for x value lines!(ax12, datetimes, values4datetimes) lines!(ax22, datetimes, values4datetimes) lines!(ax32, datetimes, values4datetimes) # Add an Axis with and without Time formatting on X axis ax13 = Axis( fig[1, 3], xticks = WilkinsonTicks(4; k_min = 4), title = rich("Time format on X axis with ", rich("Automatic", color = :blue, font = "Consolas"), " as formatter"), ) ax23 = Axis( fig[2, 3], xticks = WilkinsonTicks(4; k_min = 4), xtickformat = dateformat"S.s\s", title = rich( "Time format on X axis with ", rich("dateformat\"S.s\\s\"", color = :blue, font = "Consolas"), " as formatter", ), ) ax33 = Axis( fig[3, 3], xticks = WilkinsonTicks(4; k_min = 4), xtickformat = (V -> [string(round(Int64, v) ÷ 1_000_000) * "ms" for v in V]), title = rich( "Time format on X axis with function\n", rich("V -> [string(round(Int64, v) ÷ 1_000_000) * \"ms\" for v in V]\n", color = :blue, font = "Consolas"), "as formatter", ), ) # Create a line plot with Time type for x value lines!(ax13, times, values) lines!(ax23, times, values) lines!(ax33, times, values) # Show the plot display(fig) # Save the plot save("Feature_Request_example1.png", fig) ```

Example output

Further enhancement

It would be great to have a default (Automatic ?) well behaved Date, Time, DateTime formatter like the one in Plotly.js

oscarvdvelde commented 1 month ago

I have bug associated with this:
Formatting an axis in which you plot Time data. In my case (every 5 minutes from 03:30 to 6:30) I only get three ticks. Specifying xticks = LinearTicks(10), for example, before or after, is ignored.
Besides that, a good way to control date and time ticks may be to be able to specify them as follows: xticks = DateTimeTicks(Minute(15))