plotly / dash-core-components

OBSOLETE: now part of https://github.com/plotly/dash
https://dash.plotly.com
MIT License
270 stars 147 forks source link

Updating dcc.Location pathname #44

Open tahouse opened 6 years ago

tahouse commented 6 years ago

I've followed the updated urls example at https://plot.ly/dash/urls and have found it very helpful in setting up some rudimentary routing. I'd like to take it a bit further and allow changes made within a page to update the pathname.

From the example:

@app.callback(dash.dependencies.Output('page-content', 'children'),
              [dash.dependencies.Input('url', 'pathname')])
def display_page(pathname):
    return html.Div([
        html.H3('You are on page {}'.format(pathname))
    ])

where the page-content is changed when the url pathname is updated. This works well, allowing me to set default UI states based on the pathname.

What I'd like to do is reverse the situation -- UI changes update the pathname -- on input from one of several internal divs, update the url pathname to reflect the current settings being used. The Link component updates the pathname, but only on click. I'd like the update to be based on graph update callbacks.

For instance (some made up pathname):

@app.callback(
    Output('url', 'pathname'),
    [Input('display-charts-rooms', 'children'),
    Input('display-charts-room', 'children'),])
def update_pathname(graphs1,graphs2):
    # outputString=get_current_settings_madeUpFunction(graphs1,graphs2)
    return '/changesMade/settings=ThisAndThat'

This doesn't seem to work however. I don't get any errors or exceptions; but nothing happens.

With this, someone could update the the UI widgets, adjust the output graphs, all the changes could reflect in the current path name like: http://plot_server:8050/plot/dataset=1/startDate=2017-06-07/endDate=2017-06-09/

Passing this off to another user (email) would mean quick sharing of the same graph setup parameters, without having to go through tedious setup procedures (click this, wait for component to update, click next component, select multiple items from dropdown box, adjust date range 1, adjust date range 2). This seems to be inline with making dash apps more stateful, except in this case, for the user. To quote from @chriddyp on an unrelated issue:

I think that in most cases, the Dash app developer shouldn't need to know the order of events. If they do, it usually means that the component needs to be patched in a way to make it more stateful or the UI needs to be redesigned. I'll leave this issue open to invite examples of good UIs that are impossible to create without supporting "order-of-events".

A downside would be the need to prevent some feedback loop: user inputs a pathname, callback takes pathname and updates page content, page content update triggers pathname change, and so on.

Other than that, I'm not knowledgeable enough with the underlying react js components to try changing them. The dcc.Location component appears simpler than other components that have update methods, but maybe I'm looking in the wrong spot:

componentWillReceiveProps(nextProps) {
        this.updateLocation(nextProps);
    }

    render() {
        return null;

As of right now, the only option I can see for easy setup/testing urls is to generate a text string as described, but output it to some P tag on screen so that a user can copy it and paste it.

Any ideas, or am I thinking about this all wrong? Thanks for any help and for the great package.

chriddyp commented 6 years ago

Thanks for opening @tahouse ! Yes, this would be great and you're correct that this isn't supported right now.

Dash doesn't support "synced" components (there is a better name for this but I can't recall what it is, maybe "two way bindings" would be a better way to describe it.)

In the case for URLs, we need the following flows: URL -> Updates Component Component Changes -> Updates URL

If you think about this in reactive terms like as in an Excel spreadsheet, this would cause an infinite loop - changing the URL updates the component which then updates the URL which then updates the component, etc.

What Dash should do is update the components and then stop when it detects a loop like this. Or, allow the user to configure the number of loops that it should execute before stopping (like a discrete dynamical system that eventually converges).

Then, you could write these relationships that depend on each other.


This actually comes up a lot. If we want to enable users to write and share complex models with "what if" scenarios where every parameter is configurable, then we'll need to support this. For example, pulling from Bret Victor's Explorable Explanations (http://worrydream.com/ExplorableExplanations/):

Suppose that an extra $17 was charged to 85% of vehicle registrations. Park admission would be free for those who paid the charge. This would collect an extra $338 million for a total state park budget of $738 million.

In that case, every word that is in the code syntax would be variable: you could modify any of those parameters and see the rest of the parameters update. This would require a lot of coding - for each component (you'd need to solve for the inverse relationship of that parameter and your UI would have to fix some variables in those inverse relationships), but it might be pretty cool.


Long story short, I'd like to support this use case. In the meantime, I'm not aware of any other workarounds.

nkrumm commented 6 years ago

I would love this feature as well. I've tried playing with the internals dcc.Location component, but always ended up with various infinite loops or dead-ends.

Recently just saw the addition of the state input callback, and thought that this might be a solution to the problem here... but before embarking on attempting that I thought I would circle back here and see if any progress or changes were planned to the URL/location component.

chriddyp commented 6 years ago

So, I think that ideally we would allow circular relationships like this:

# clicking on radio item updates the URL
@app.callback(Output('location', 'pathname'), [Input('radio', 'value')])
def update_pathname(value):
     return '/{}'.format(value)

# landing on a page directly updates the radio item navigator
@app.callback(Output('radio', 'value'), [Input('location', 'pathname')])
def sync_pathname(value):
     return value.strip('/')

With support for previous state (https://github.com/plotly/dash-renderer/pull/25), we would be able to eventually do something like:

@app.callback(
    Output('store', 'children'),
    [Input('radio', 'value'), Input('location', 'pathname')],
    [PrevState('radio', 'value'), PrevState('location', 'pathname')])
def update_current_location(radio, path, prevRadio, prevPath):
    if radio != prevRadio:
         return '/{}'.format(radio)
    elif path != prevPath:
         return path

@app.callback(Output('radio', 'value'), [Input('store', 'children')])
def update_radio(path):
    return path

@app.callback(Output('location', 'pathname'), [Input('store', 'children')])
def update_path(path):
    return path

Which might make the code a little bit cleaner (easier to add more items that depend on the current path). However, we still run into the issue of having circular dependencies.

Spin45 commented 6 years ago

Just adding another vote for this feature.

I believe this is generally referred to as "deep linking" (at least that's what Adobe Flex called it) and when applied to a web application is specifically referring to RESTful invocation of an application's state via its URL with the updating of the URL as the application is used, so as to continue to allow the back button in browsers to work (core usability) and states of the web application to be shared via URL as mentioned above.

We are hoping to build a general purpose Plotly app for our Earth science data visualization applications that can be launched from our other web applications via RESTful URLS (I believe this already works). However, we want to make our Plotly app powerful and configurable and not being able to preserve the Plotly app's state after many operations, via an updated URL is an issue.

chriddyp commented 6 years ago

I believe this is generally referred to as "deep linking" (at least that's what Adobe Flex called it) and when applied to a web application is specifically referring to RESTful invocation of an application's state via its URL with the updating of the URL as the application is used, so as to continue to allow the back button in browsers to work (core usability) and states of the web application to be shared via URL as mentioned above.

I think that this is actually a separate issue than circular states, in that it's more of a built-in "save state in url" https://github.com/plotly/dash/issues/188

syamajala commented 6 years ago

Any updates on this issue?

In the latest release, I tried to update a location pathname as the output of callback and ended up stuck in an infinite loop.

EDIT: I posted an example of what I'm trying to do here: https://community.plot.ly/t/multiple-pages-and-html-forms/10411

bpostlethwaite commented 6 years ago

Hi @syamajala thanks for posting. Better Location handling is on the short-term roadmap but I can't guarantee any particular dates right now. We'll update this thread once we have more info

glennlawyer commented 5 years ago

Upvoting this. My particular use case is selecting the URL from a dropdown, because we have more pages than fits in a menu bar. I was able to create the infinite loop pretty easily by having the dropdown update the url, and the url update the page (which re-loaded the header with the dropdown)...

Zylatis commented 4 years ago

Upvoting this as I would like to have a multi-user app where the URL is the user information I need to feed into the callbacks to display the correct things.

NikTheGeek1 commented 4 years ago

Upvoting here as well

simone-pignotti commented 4 years ago

Upvoting too! Thanks @chriddyp for the thorough explanation.

usser123 commented 4 years ago

Upvoting too.

I have tried to redirect to a specific page after successful login. I encountered problems with both the ways that I tried:

  1. By using the callback where url is both the input and the output, I received a dash exception.
  2. By using dcc.Location in the callback function, the url does not actually get updated in the browser. Same problem as described in the very first post in this thread.
rsandler00 commented 4 years ago

Any updates on this? Would also love to have!

jimkrooskos commented 4 years ago

Looking for a solution for this as well!

codepatel commented 3 years ago

I came across this issue, when I was searching for the same feature in Dash API. I agree with @chriddyp on the Circular dependency between the URL.pathname and Layout Elements that are data-triggered and data-driven. I will share a workaround for the very common use-cases in sharing and collaborating Dashboards (also posted on this Medium story: https://medium.com/@hardiksp/how-to-solve-the-dependency-when-sharing-and-collaborating-on-useful-dashboard-apps-with-the-dash-d136a21023e0)

  1. You want the Dashboard author to share with Users the deployed app layout and data analysis presentation (with a custom URL served to get to the "State" of Dashboard) - This is done by parsing the URL and then using the Redis key/value pair as an in-memory data structure store. Key will be a session-based UUID and can optionally also have URL pieces to search for inspection or data-crunching.
  2. Now, users with this "State" of Dashboard will want to make more modifications like what-if scenarios on the inputs that will trigger output callbacks. To save this "new" what-if state of the Dashboard app, there is a user button, that will take the dcc.Store component's saved output callback values, to save to a Redis data store.
  3. This user-action will generate a new URL shown with a dcc.Link component that can be used to rinse and repeat the cycle. image

Gist for this UI callbacks

Here are the links to:

  1. The deployed app: https://dcf-valuation-damodaran.herokuapp.com/apps/dcf/AAPL
  2. Source code repo: https://github.com/codepatel/dcf#how-to-setup-this-app-locally

Note, I didn't save all the output callback values in the data cache store in Redis, only the final output states of the data that was important and not output figures or tables. This can be easily done for figures' state by also saving the dict for fig.data and fig.layout in a dcc.Store component. Ideally, if dcc has a future way to make this process more abstracted with all output callbacks in the app layout, rather than the user having to pick and choose output callback to save to a cache, then any Dashboard state can be restored given a unique, custom URL in sharing and collaboration. (I used Redis to cache data as I was new to learning it, but a filesystem cache can also be used with the dict key/pair management).

Please let me know if any feedback on the above approach for suggestions.

ldorigo commented 1 year ago

Hi,

Is there still no way to do this? I have tried updating the location with:

@app.callback(
          Output("location", "pathname")
          Input(f"test", "value")
)
def update_location(val):
    return "foo"

And it returns correctly but doesn't actually change the in-browser URL.

ned2 commented 1 year ago

Hey @ldorigo, this repo is no longer used; it's been migrated into https://github.com/plotly/dash