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

Custom DateTimePicker component #901

Open
1 task done
gtauzin opened this issue Nov 27, 2024 · 15 comments
Open
1 task done

Custom DateTimePicker component #901

gtauzin opened this issue Nov 27, 2024 · 15 comments
Assignees
Labels
Custom Components 🚀 Issue contains a custom component request

Comments

@gtauzin
Copy link

gtauzin commented Nov 27, 2024

Question

Hello,

I am trying to create a custom DateTimePicker component. Although dmc has one, it is available in v0.15 of dmc which is not available (yet?) in vizro. Can I first ask if anyone is aware of an open source version of such a vizro component? Seems like it should be pretty commonly used but could not find anything.

My idea was to take a DatePicker and a TimeInput both from dmc and put them next to each other in a group. However, I am running into the following issue:

  • One of the parameter of the component would be value, which is a datetime.datetime. At build time, I pass value.date() to DatePicker and value.time() to TimeInput. However, when it is modified, I would like to recompute value from the date_picker.value and the time_input.value. Is it possible?

Thank you for your help!

Code/Examples

This is what I came up with so far on PyCafe.

Which package?

vizro

Code of Conduct

@gtauzin gtauzin added General Question ❓ Issue contains a general question Needs triage 🔍 Issue needs triaging labels Nov 27, 2024
@maxschulz-COL
Copy link
Contributor

Hey @gtauzin 👋 ,

nice to meet you? At the risk of not having understood your question, but isn't this the solution: https://vizro.readthedocs.io/en/stable/pages/API-reference/models/#vizro.models.DatePicker

Let me know if you were looking for something else 💪 !

Best,

Max

@maxschulz-COL maxschulz-COL removed the Needs triage 🔍 Issue needs triaging label Nov 27, 2024
@gtauzin
Copy link
Author

gtauzin commented Nov 27, 2024

Hi @maxschulz-COL and nice to meet you too!

vizro's DatePicker only allows to pick date and not time. I need to pick both of them.

dash-maintine-components has a DateTimePicker but only in its most recent version (>= 0.15.0) which is incompatible with vizro (requires <0.13.0 see here). That lead me to create my own by putting together two existing components of dash-mantine-components, DatePicker (on which vizro's DatePicker relies) and TimeInput.

@maxschulz-COL
Copy link
Contributor

Hi @maxschulz-COL and nice to meet you too!

vizro's DatePicker only allows to pick date and not time. I need to pick both of them.

dash-maintine-components has a DateTimePicker but only in its most recent version (>= 0.15.0) which is incompatible with vizro (requires <0.13.0 see here). That lead me to create my own by putting together two existing components of dash-mantine-components, DatePicker (on which vizro's DatePicker relies) and TimeInput.

Ah I see, in that case I think @antonymilne has a potential solution for you :)

@gtauzin
Copy link
Author

gtauzin commented Nov 28, 2024

Thanks @maxschulz-COL, that would be great!

@antonymilne
Copy link
Contributor

antonymilne commented Nov 28, 2024

Hello @gtauzin and thanks for raising the issue. This is all very interesting. And thanks for the pycafe example that really helps understand what's going on.

For a start, the dmc date picker components have caused us various problems and workarounds (as you can see by the comments), and about 6 months ago when we fixed stuff well enough we pinned to dmc<0.13.0 because we knew that future breaking changes were likely in dmc. I didn't know that 0.15 had now come out, which is good to know. I see that 0.12 still seems quite popular (more so than 0.14 in fact): https://pepy.tech/projects/dash-mantine-components?versions=0.14.*&versions=0.13.*&versions=0.12.
image

In general, much as I love dmc, we are reducing our dependence on it (in favour of dbc) and I suspect the datepicker will be the only dmc component we have left soon.

All this to say: we should try and bump our dmc dependency, but it's not a high priority I'm afraid. Possibly it would help with some of the date picker issues though @petar-qb? Which would offer some more incentives to make the change. I've made a ticket to try and see how easy it is to bump anyway: #905.

When we do bump our dependency it remains to be seen if and how we would be able to build DatePickerTime into Vizro. It's not something we've thought about before and you're the first person who's asked for any kind of time (rather than just date) selector but let me add it to #318. It's a very reasonable request but just not something anyone has asked before.

So I think for now the best approach is indeed to try and make your own custom component with DatePicker and TimeInput, just like you've done here.

Now onto your question: if I understand correctly, you'd like the default value shown by the date and time picker to correspond to today's date and the current time, is that right? I think something like this should be possible, although I suspect it might be a bit buggy or you might need to sacrifice something like persistence of value when you change pages. Are you using dynamic data?

@gtauzin
Copy link
Author

gtauzin commented Nov 28, 2024

Hi @antonymilne. Thank you, I really appreciate the time you took to give me a better picture of the situation.

I do not mind building a custom DateTimePicker as it is also a good way to improve my understanding of vizro. I do use dynamic data (through the kedro data catalog) but I am not sure how it is relevant to this custom component.

In any case, let me try and explain more clearly the issue I am facing. When I create a DateTimePicker, I can pass a value which is datetime object and through the build function I can assign the DatePicker's value to be the date (i.e. I pass date_value=value.date() and the TimeInput's value to be the time (i.e. time_valuevalue.time()). This is fine and works well.

Now, I wish to use the value of my DateTimePicker in different actions. My problem is that when the user modify the date and the time, they modify it separately in two different subcomponent, I would like to pass the value that is built back from the date_value and time_value into a datetime object.

Thank you and please let me know if this is not clear!

@antonymilne
Copy link
Contributor

antonymilne commented Nov 29, 2024

Ahhh ok, this makes sense I think.

Having two selectors in one like you were trying was a good idea but I think might be harder to get working than having two separate selectors: one for a date and one for a time. At the moment we have a pretty hard assumption that each control can only pass a single value to the backend, so a composite selector that needs to somehow combine values from two different selectors sounds tricky. There's some workarounds that would make this possible, but probably easier to do as two separate selectors.

Take a look at the below example on pycafe. It uses a custom figure just as a proof of concept to show how it works.

There's a few gotchas with this:

  • since they're separate components, whenever you change either date or time, the targeted components will update. This might be good or bad depending on your application.
  • dmc.TimeInput seems to not be able to render the initial value correctly, not sure why - maybe a bug on the dmc side, which is hopefully fixed in later versions. It works fine when you manually enter numbers though.
  • since dmc handles date and time to be in certain string formats, most of the complexity here is to do with parsing different formats to and from Python datetime
  • the on screen rendering isn't as nice as your version since the date and time are directly underneath each other rather than next to each other. This will be much easier to achieve with CSS once [Feat] Recognise controls outside page.controls #903 has been merged (use a control group like in Ability to create control headers with horizontal rulers #82)
class TimePicker(VizroBaseModel):
    """Temporal single option selector `TimePicker`.

    Can be provided to [`Filter`][vizro.models.Filter] or [`Parameter`][vizro.models.Parameter].
    Based on [`dmc.TimeInput`](https://www.dash-mantine-components.com/components/timeinput).

    Args:
        type (Literal["time_picker"]): Defaults to `"time_picker"`.
        with_seconds (bool): Whether to show seconds in the time picker. Defaults to `False`.
        value (Optional[date]): Default time for time picker. Defaults to `None`.
        title (str): Title to be displayed. Defaults to `""`.
        actions (list[Action]): See [`Action`][vizro.models.Action]. Defaults to `[]`.

    """

    type: Literal["time_picker"] = "time_picker"
    with_seconds: bool = Field(False, description="Whether to show seconds in the time picker.")
    value: Optional[time] = Field(None, description="Default date for datetime picker")
    title: str = Field("", description="Title to be displayed.")
    actions: list[Action] = []

    _input_property: str = PrivateAttr("value")
    _set_actions = _action_validator_factory("value")

    def build(self):
        time_picker = dmc.TimeInput(
            id=self.id,
            value=self.value.strftime("%H:%M:%S"),
            persistence=True,
            persistence_type="session",
            withSeconds=self.with_seconds,
            className="timeinput",
        )

        return html.Div(
            children=[
                dbc.Label(children=self.title, html_for=self.id) if self.title else None,
                time_picker,
            ],
        )


vm.Parameter.add_type("selector", TimePicker)


@capture("figure")
def give_date_time(data_frame, date=None, time=None):
    # date and time come in as strings from frontend, so need to convert them back into
    # datetime types before using them.
    processed_time = parser.parse(time).time() if time is not None else None
    processed_date = parser.parse(date) if date is not None else None
    combined = datetime.combine(processed_date, processed_time) if date is not None and time is not None else None

    return dcc.Markdown(f"""
    **Raw date**: {date}

    **Processed date**: {processed_date}

    **Raw time**: {time}

    **Processed time**: {processed_time}

    **Combined**: {combined}
    """)


page = vm.Page(
    title="Vizro on PyCafe",
    components=[
        vm.Figure(id="figure", figure=give_date_time(pd.DataFrame())),
    ],
    controls=[
        vm.Parameter(
            targets=["figure.date"],
            selector=vm.DatePicker(
                title="Date",
                value=date.today(),
                min=date.today() - timedelta(days=30),
                max=date.today() + timedelta(days=30),
                range=False,
            ),
        ),
        vm.Parameter(
            targets=["figure.time"],
            selector=TimePicker(
                title="Time",
                value=datetime.now().time(),
                with_seconds=True,
            ),
        ),
    ],
)

@gtauzin
Copy link
Author

gtauzin commented Nov 29, 2024

Thank you so much for your help @antonymilne !

There's a few gotchas with this:

  • since they're separate components, whenever you change either date or time, the targeted components will update. This might be good or bad depending on your application.
  • dmc.TimeInput seems to not be able to render the initial value correctly, not sure why - maybe a bug on the dmc side, which is hopefully fixed in later versions. It works fine when you manually enter numbers though.
  • since dmc handles date and time to be in certain string formats, most of the complexity here is to do with parsing different formats to and from Python datetime
  • the on screen rendering isn't as nice as your version since the date and time are directly underneath each other rather than next to each other. This will be much easier to achieve with CSS once [Feat] Recognise controls outside page.controls #903 has been merged (use a control group like in Ability to create control headers with horizontal rulers #82)

Indeed, points 1 and 4 (along with the additional complexity) were the reason why I tried to combine DatePicker and TimeInput into a single vizro component. I'll go with your solution and try an improve the rendering once the PRs you mentioned are merged. But I guess once dmc is bumped, then I should be able to easily create a custom vizro component, right?

Thank you again :)

@antonymilne
Copy link
Contributor

The PR has merged and we've just done a release, so an improved rendering is now possible with vizro>=0.1.29 🎉 It's a non-breaking release so you should be able to do pip install -U vizro without any problems.

With this release it's now possible to nest controls inside another model inside Page.controls. So a very easy way to get a datepicker next to a timepicker is to wrap them inside a vm.Container and use layout grid to stack them next to each other with grid = [[0, 1]] (full credit to @maxschulz-COL for this idea!). Here's the relevant code and pycafe link:

vm.Parameter.add_type("selector", TimePicker)
vm.Page.add_type("controls", vm.Container)
vm.Container.add_type("components", vm.Parameter)

page = vm.Page(
    title="Vizro on PyCafe",
    components=[
        vm.Figure(id="figure", figure=give_date_time(pd.DataFrame())),
    ],
    controls=[
        vm.Container(
            id="date-time-picker",
            title="Date time picker",  # Need to give a title in the Container model but I hid it with CSS
            layout=vm.Layout(grid=[[0, 1]]),
            components=[
                vm.Parameter(
                    targets=["figure.date"],
                    selector=vm.DatePicker(
                        title="Date",
                        value=date.today(),
                        min=date.today() - timedelta(days=30),
                        max=date.today() + timedelta(days=30),
                        range=False,
                    ),
                ),
                vm.Parameter(
                    targets=["figure.time"],
                    selector=TimePicker(
                        title="Time",
                        value=datetime.now().time(),
                        with_seconds=True,
                    ),
                ),
            ]
        )
    ],
)

Alternatively you might like to just make a whole new model for this which nests the parameters:

class DateTimeParameters(VizroBaseModel):
    type: Literal["date_time_picker"] = "date_time_picker"
    # expose whatever fields you like - could have `min_date`, `max_date`. Or pass in whole date_picker object
    
    def build(self):
        # wrap two parameters in a group with html.Div or dmc.Group or whatever you like
        return html.Div(id=self.id, children=[vm.Parameter(vm.DatePicker(...)).build(), vm.Parameter(TimePicker(...)).build()])

The Container approach is easiest if a grid already gives you the control you need; the DateTimeParameters is more flexible and customisable but would take more playing around with CSS.

@antonymilne antonymilne added Custom Components 🚀 Issue contains a custom component request and removed General Question ❓ Issue contains a general question labels Dec 3, 2024
@antonymilne
Copy link
Contributor

antonymilne commented Dec 3, 2024

Indeed, points 1 and 4 (along with the additional complexity) were the reason why I tried to combine DatePicker and TimeInput into a single vizro component. I'll go with your solution and try an improve the rendering once the PRs you mentioned are merged. But I guess once dmc is bumped, then I should be able to easily create a custom vizro component, right?

On point 1 ("since they're separate components, whenever you change either date or time, the targeted components will update. This might be good or bad depending on your application"), I am curious actually, since after thinking about it some more I would guess that the behaviour of separate components here was actually what most people wanted. Just to understand, why is this instant update not the right behaviour for your app?

Indeed once we bump dmc it should be easy to make a custom DateTimePicker (or maybe we'll even build one into vizro eventually and you won't need any custom code). We have a ticket #905 to investigate how feasible this is, but given that datepicker is the only dmc component we use at the moment (and was very hard to get working fully 😅) it is not top priority tbh.

@gtauzin
Copy link
Author

gtauzin commented Dec 3, 2024

The PR has merged and we've just done a release, so an improved rendering is now possible with vizro>=0.1.29 🎉 It's a non-breaking release so you should be able to do pip install -U vizro without any problems.

With this release it's now possible to nest controls inside another model inside Page.controls. So a very easy way to get a datepicker next to a timepicker is to wrap them inside a vm.Container and use layout grid to stack them next to each other with grid = [[0, 1]] (full credit to @maxschulz-COL for this idea!). Here's the relevant code and pycafe link:

Congratulations on the release 🎉, it seems to be a really neat improvement! I really like the fact that vizro enables this kind of structured compositions. I'll try it out :)

Alternatively you might like to just make a whole new model for this which nests the parameters:

class DateTimeParameters(VizroBaseModel):
    type: Literal["date_time_picker"] = "date_time_picker"
    # expose whatever fields you like - could have `min_date`, `max_date`. Or pass in whole date_picker object
    
    def build(self):
        # wrap two parameters in a group with html.Div or dmc.Group or whatever you like
        return html.Div(id=self.id, children=[vm.Parameter(vm.DatePicker(...)).build(), vm.Parameter(TimePicker(...)).build()])

The Container approach is easiest if a grid already gives you the control you need; the DateTimeParameters is more flexible and customisable but would take more playing around with CSS.

Interesting. It is very similar to the composite DateTimePicker I had written in my first post (see PyCafe) and I indeed had to play a bit with CSS. I am curious: is there any difference with mine in terms of usability?

@gtauzin
Copy link
Author

gtauzin commented Dec 3, 2024

On point 1 ("since they're separate components, whenever you change either date or time, the targeted components will update. This might be good or bad depending on your application"), I am curious actually, since after thinking about it some more I would guess that the behaviour of separate components here was actually what most people wanted. Just to understand, why is this instant update not the right behaviour for your app?

At the moment, my app fetches data from the cloud based on date and time picked. Updating them separately means that if only one is properly set you fetch the wrong data.

Note that in practice, my situation is worse as I have a start date/time and an end date/time.

There is a simple workaround which is to add a button "Fetch data" and have the user click it once both date and time are set, but I initially thought this could be avoided.

Indeed once we bump dmc it should be easy to make a custom DateTimePicker (or maybe we'll even build one into vizro eventually and you won't need any custom code). We have a ticket #905 to investigate how feasible this is, but given that datepicker is the only dmc component we use at the moment (and was very hard to get working fully 😅) it is not top priority tbh.

So being able to make a custom DateTimePicker is easy but integrating a DateTimePicker into vizro is hard. I naively thought this would be the same.

Thank you so much for your help. I love what you guys are doing with vizro, this really helps me having much cleaner app code.

@antonymilne
Copy link
Contributor

Interesting. It is very similar to the composite DateTimePicker I had written in my first post (see PyCafe) and I indeed had to play a bit with CSS. I am curious: is there any difference with mine in terms of usability?

Yeah, I was actually very impressed with your CSS because these dmc components in particular are not so easy to handle in vizro, so you did a great job of it! There are basically two key differences compared to your version:

  • composing DatePicker and TimePicker is probably better than copying and pasting the code from the DatePicker model, since when we update DatePicker in future you will get those updates consistently rather than needing to rewrite your code
  • my above example uses two vm.Parameters rather than putting the DateTimePicker directly in controls. This is what enables it to function as a parameter without needing to manually assign actions to the selector you've provided. It basically means you can use the same vm.Parameter(targets=...) syntax and it will work right out of the box.

At the moment, my app fetches data from the cloud based on date and time picked. Updating them separately means that if only one is properly set you fetch the wrong data.

Note that in practice, my situation is worse as I have a start date/time and an end date/time.

There is a simple workaround which is to add a button "Fetch data" and have the user click it once both date and time are set, but I initially thought this could be avoided.

Thanks for explaining, this makes sense! Actually I'm not sure that the combined DateTimePicker in dmc 0.15 would be able to achieve this for you - if you look at the first example in their docs then after clicking the tick initially, it seems to update instantly even when you change just the date or time without clicking the tick again. Not sure whether that's intentional or not, but I don't see any options to change it to "wait until tick is clicked to apply change".

So yes, for now you will indeed have to have a "Fetch data" button that's manually pressed I'm afraid. Possibly in future we could add some kind of functionality that allows you to have an "apply" button attached to a control group, and none of the controls in that group will trigger until manually applied. But that will not happen for a long time I think.

So being able to make a custom DateTimePicker is easy but integrating a DateTimePicker into vizro is hard. I naively thought this would be the same.

Thank you so much for your help. I love what you guys are doing with vizro, this really helps me having much cleaner app code.

Thanks for your comments, that's much appreciated! 🙏

@antonymilne
Copy link
Contributor

antonymilne commented Dec 3, 2024

One more thought I just had on "wait until applying changes": you might be able to use debounce to help a bit here. It looks like it's available in dmc 0.12 although I'm not sure if it offers exactly the same functionality as in 0.15. For 0.15 the docs say:

debounce: (boolean | number; default False): If True, changes to input will be sent back to the Dash server only on enter or when losing focus. If it's False, it will send the value back on every change. If a number, it will not send anything back to the Dash server until the user has stopped typing for that number of milliseconds.

For 0.12 it says:

debounce (number; optional): Debounce time.

I don't know whether the change in docs are actually new functionality or if it's just improved docs.

@gtauzin
Copy link
Author

gtauzin commented Dec 11, 2024

Yeah, I was actually very impressed with your CSS because these dmc components in particular are not so easy to handle in vizro, so you did a great job of it! There are basically two key differences compared to your version:

  • composing DatePicker and TimePicker is probably better than copying and pasting the code from the DatePicker model, since when we update DatePicker in future you will get those updates consistently rather than needing to rewrite your code
  • my above example uses two vm.Parameters rather than putting the DateTimePicker directly in controls. This is what enables it to function as a parameter without needing to manually assign actions to the selector you've provided. It basically means you can use the same vm.Parameter(targets=...) syntax and it will work right out of the box.

Thank you, this is very clear and makes a lot of sense!

Thanks for explaining, this makes sense! Actually I'm not sure that the combined DateTimePicker in dmc 0.15 would be able to achieve this for you - if you look at the first example in their docs then after clicking the tick initially, it seems to update instantly even when you change just the date or time without clicking the tick again. Not sure whether that's intentional or not, but I don't see any options to change it to "wait until tick is clicked to apply change".

One more thought I just had on "wait until applying changes": you might be able to use debounce to help a bit here. It looks like it's available in dmc 0.12 although I'm not sure if it offers exactly the same functionality as in 0.15.

Ah! That's a very good point. That was some wishful thinking on my side as I did not know about debounce. I will look into it once dmc is bumped. In the meantime, I will use your solution along with a button!

So yes, for now you will indeed have to have a "Fetch data" button that's manually pressed I'm afraid. Possibly in future we could add some kind of functionality that allows you to have an "apply" button attached to a control group, and none of the controls in that group will trigger until manually applied. But that will not happen for a long time I think.

This is clear, thank you so much for your help on this.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Custom Components 🚀 Issue contains a custom component request
Projects
None yet
Development

No branches or pull requests

3 participants