0

Consider a plotly figure where you can select polynomial features for a line fit using JupyterDash:

enter image description here

If you select an area and then choose another number for polynomial features, the figure goes from this:

enter image description here

... and back to this again:

enter image description here

So, how can you set things up so that the figure displays the same area of the figure every time you select another number of features and trigger another callback?

Complete code:

import numpy as np
import plotly.express as px
import plotly.graph_objects as go
from jupyter_dash import JupyterDash
import dash_core_components as dcc
import dash_html_components as html
from dash.dependencies import Input, Output

from sklearn.preprocessing import PolynomialFeatures 
from sklearn.linear_model import LinearRegression
from sklearn.pipeline import make_pipeline

from IPython.core.debugger import set_trace

# Load Data
df = px.data.tips()
# Build App
app = JupyterDash(__name__)
app.layout = html.Div([
    html.H1("ScikitLearn: Polynomial features"),
    dcc.Graph(id='graph'),
    html.Label([
        "Set number of features",
        dcc.Slider(id='PolyFeat',
    min=1,
    max=6,
    marks={i: '{}'.format(i) for i in range(10)},
    value=1,
) 
    ]),
])

# Define callback to update graph
@app.callback(
    Output('graph', 'figure'),
    [Input("PolyFeat", "value")]
)

def update_figure(nFeatures):
    
    global model

    # data
    df = px.data.tips()
    x=df['total_bill']
    y=df['tip']

    # model
    model = make_pipeline(PolynomialFeatures(nFeatures), LinearRegression())
    model.fit(np.array(x).reshape(-1, 1), y)
    x_reg = x.values
    y_reg = model.predict(x_reg.reshape(-1, 1))
    df['model']=y_reg

    # figure setup and trace for observations
    fig = go.Figure()
    fig.add_traces(go.Scatter(x=df['total_bill'], y=df['tip'], mode='markers', name = 'observations'))

    # trace for polynomial model
    df=df.sort_values(by=['model'])
    fig.add_traces(go.Scatter(x=df['total_bill'], y=df['model'], mode='lines', name = 'model'))
    
    # figure layout adjustments
    fig.update_layout(yaxis=dict(range=[0,12]))
    fig.update_layout(xaxis=dict(range=[0,60]))
    #print(df['model'].tail())
    fig.update_layout(template = 'plotly_dark')
    
    return(fig)

# Run app and display result inline in the notebook
app.enable_dev_tools(dev_tools_hot_reload =True)
app.run_server(mode='inline', port = 8040, dev_tools_ui=True, #debug=True,
              dev_tools_hot_reload =True, threaded=True)
vestland
  • 55,229
  • 37
  • 187
  • 305
  • Does this answer your question? [Freeze plotly-dash graph visualization](https://stackoverflow.com/questions/62267830/freeze-plotly-dash-graph-visualization) – emher Sep 14 '20 at 06:33
  • @emher Close, but not quite. I found the title to be a bit misleading with the `freeze` part. This makes it sound like the figure is unresponsive to all changes. Adding to that, your suggestion is `fig.update_layout(uirevision='some-constant')` which will likely lead readers to belive that it's a placeholder for some *numerical* constant. It turns out that any string will work though, so `fig.update_layout(uirevision='some-constant')` isn't *wrong*. Even `fig.update_layout(uirevision='wrong')` will trigger the same functionality. – vestland Sep 14 '20 at 07:29
  • @emher But the primary reason I wrote up this question is that it did not pop up when searching for `[plotly] uirevision`. But I see now that your linked post lacked the `[plotly]` tag. I've fixed that now. – vestland Sep 14 '20 at 07:31
  • @emher Your last statement in your answer `Whenever you need to reset the view, change the value to something else.` is not quite correct though, as per my first comment. – vestland Sep 14 '20 at 07:32
  • Ah, yes, i see how my phrasing could be misunderstood. I have edited the answer to make it more clear. However, i am not sure that i understand why 'Whenever you need to reset the view, change the value to something else.' is not correct? – emher Sep 14 '20 at 08:44
  • @emher Try changing `'some-constant'` to `'hello-world'`. It seems that uirevevision is set to `constant` as long as there is a random string there. In order to actually reset the viewing behaviour, you'll have to remove `fig.update_layout(uirevision='constant')` all togheter or set `fig.update_layout(uirevision='')`. – vestland Sep 14 '20 at 08:51

1 Answers1

1

This is surprisingly easy and just adds to the power and flexibility of Plotly and Dash. Just add

fig.update_layout(uirevision='constant')

to your code and you're good to go:

enter image description here

For even more control you can set axis properties directly. From the docs:

For finer control you can set these sub-attributes directly. For example, if your app separately controls the data on the x and y axes you might set xaxis.uirevision=*time* and yaxis.uirevision=*cost*. Then if only the y data is changed, you can update yaxis.uirevision=*quantity* and the y axis range will reset but the x axis range will retain any user-driven zoom.

Complete code:

import numpy as np
import plotly.express as px
import plotly.graph_objects as go
from jupyter_dash import JupyterDash
import dash_core_components as dcc
import dash_html_components as html
from dash.dependencies import Input, Output

from sklearn.preprocessing import PolynomialFeatures 
from sklearn.linear_model import LinearRegression
from sklearn.pipeline import make_pipeline

from IPython.core.debugger import set_trace

# Load Data
df = px.data.tips()
# Build App
app = JupyterDash(__name__)
app.layout = html.Div([
    html.H1("ScikitLearn: Polynomial features"),
    dcc.Graph(id='graph'),
    html.Label([
        "Set number of features",
        dcc.Slider(id='PolyFeat',
    min=1,
    max=6,
    marks={i: '{}'.format(i) for i in range(10)},
    value=1,
) 
    ]),
])

# Define callback to update graph
@app.callback(
    Output('graph', 'figure'),
    [Input("PolyFeat", "value")]
)

def update_figure(nFeatures):
    
    global model

    # data
    df = px.data.tips()
    x=df['total_bill']
    y=df['tip']

    # model
    model = make_pipeline(PolynomialFeatures(nFeatures), LinearRegression())
    model.fit(np.array(x).reshape(-1, 1), y)
    x_reg = x.values
    y_reg = model.predict(x_reg.reshape(-1, 1))
    df['model']=y_reg

    # figure setup and trace for observations
    fig = go.Figure()
    fig.add_traces(go.Scatter(x=df['total_bill'], y=df['tip'], mode='markers', name = 'observations'))

    # trace for polynomial model
    df=df.sort_values(by=['model'])
    fig.add_traces(go.Scatter(x=df['total_bill'], y=df['model'], mode='lines', name = 'model'))
    
    # figure layout adjustments
    fig.update_layout(yaxis=dict(range=[0,12]))
    fig.update_layout(xaxis=dict(range=[0,60]))
    #print(df['model'].tail())
    fig.update_layout(template = 'plotly_dark')
    fig.update_layout(uirevision='constant')
    return(fig)

# Run app and display result inline in the notebook
app.enable_dev_tools(dev_tools_hot_reload =True)
app.run_server(mode='inline', port = 8040, dev_tools_ui=True, #debug=True,
              dev_tools_hot_reload =True, threaded=True)
vestland
  • 55,229
  • 37
  • 187
  • 305