✊🏿 Black Lives Matter. Please consider donating to Black Girls Code today.
🐇 Announcing Dash VTK for 3d simulation graphics. Check out the March webinar.

Uirevision between a callback and a clientside callback

Hello! I’m a little iffy on how the uirevision attribute of a dcc.Graph layout works. Right now I have an applicion where a user selects a unit and a date, then various maps and graphs are populated. The xaxis of the graphs are a time range, which is controlled by the user by a range slider. Currently I have it so when the user selects a date, the graph for whole day is loaded, all data from that day is displayed and the entire time range is the xaxis range. To save having to rebuild the graph when the user slides the slider, a client side callback is called and sets the xaxis range to match what the user selected from the slider. My issue is it seems that the uirevision I set (the date dropdown) doesn’t seem to carry over from the client side callback.

As an example I have a boolean switch that allows the user to lock the y axes of the graph, allowing them to zoom along the xaxis with the scroll wheel without changing the range of the yaxis. When I zoom on the graph and then toggle that lock switch, the graph zooms back to what it was before.

here are the series of callbacks in question:

@app.callback(
    Output('uv-tab-content-store', 'data'),
    [Input('uv-graph-selector', 'value'),
     Input('uv-date-dropdown', 'value')],
    [State('uv-uid-dropdown', 'value')])
def update_selected_graph(selected_graph, date, uid):
    """
    Callback that creates the graph selected from the tabs. Triggered by a new time range, and the selection of tabs.
    """
    global current_figure
    if date is None:
        raise PreventUpdate

    dataframe = get_data(collection_name, date, uid)
    all_times = [datetime.fromtimestamp(entry-FOUR_HOURS) for entry in list(dataframe.timestamp)]

    if selected_graph == 'all_temps':
        try:
            air_in_temps = replace_nans(list(dataframe.lab_air_temp.isnull()), list(dataframe.lab_air_temp))
        except AttributeError:
            air_in_temps = list()
        # NOTE: Coolant in and out are temporarily switched on purpose.
        try:
            coolant_in_temps = replace_nans(list(dataframe.lab_cool_out_temp.isnull()), list(dataframe.lab_cool_out_temp))
        except AttributeError:
            coolant_in_temps = list()
        try:
            coolant_out_temps = replace_nans(list(dataframe.lab_cool_in_temp.isnull()), list(dataframe.lab_cool_in_temp))
        except AttributeError:
            coolant_out_temps = list()
        try:
            headpipe_temps = replace_nans(list(dataframe.lab_headpipe_temp.isnull()), list(dataframe.lab_headpipe_temp))
        except AttributeError:
            headpipe_temps = list()
        try:
            oil_sump_temps = replace_nans(list(dataframe.lab_oil_temp.isnull()), list(dataframe.lab_oil_temp))
        except AttributeError:
            oil_sump_temps = list()
        try:
            spark_temps = replace_nans(list(dataframe.lab_spark_temp.isnull()), list(dataframe.lab_spark_temp))
        except AttributeError:
            spark_temps = list()

        coolant_differences = list()
        for cool_out, cool_in in zip(coolant_out_temps, coolant_in_temps):
            if cool_out is not None and cool_in is not None:
                coolant_differences.append(cool_out - cool_in)
            else:
                coolant_differences.append(None)

        fig = go.Figure()
        fig.add_trace(go.Scatter(x = all_times,
                                 y = air_in_temps,
                                 name = 'Air In Temperatures',
                                 mode = 'lines',
                                connectgaps=True
                        )
        )

        fig.add_trace(go.Scatter(x = all_times,
                                 y = coolant_in_temps,
                                 name = 'Coolant In Temperatures',
                                 mode = 'lines',
                                connectgaps=True
                        )
        )

        fig.add_trace(go.Scatter(x = all_times,
                                 y = coolant_out_temps,
                                 name = 'Coolant Out Temperatures',
                                 mode = 'lines',
                                connectgaps=True
                        )
        )

        fig.add_trace(go.Scatter(x = all_times,
                                 y = coolant_differences,
                                 name = 'Coolant Temperature Differences',
                                 mode = 'lines',
                                 visible = 'legendonly',
                                 connectgaps=True
                        )
        )

        fig.add_trace(go.Scatter(x = all_times,
                                 y = headpipe_temps,
                                 name = 'Headpipe Temperatures',
                                 mode = 'lines',
                                connectgaps=True
                        )
        )

        fig.add_trace(go.Scatter(x = all_times,
                                 y = oil_sump_temps,
                                 name = 'Oil Temperatures',
                                 mode = 'lines',
                                connectgaps=True
                        )
        )

        fig.add_trace(go.Scatter(x = all_times,
                                 y = spark_temps,
                                 name = 'Spark Temperatures',
                                 mode = 'lines',
                                connectgaps=True
                        )
        )

        fig.update_layout(go.Layout(paper_bgcolor='#f9f9f9', uirevision=date,
                                    margin={'l': 40, 'b': 40, 't': 10, 'r': 100},
                                    modebar={'orientation': 'v', 'activecolor': '#00a355'},
                                    legend={'orientation': 'h'})
        )
        return fig

    elif selected_graph == 'throttle_rmp_speed':
        # Speed must be treated differently because of how it is tied to lat and lon
        speeds = list()
        try:
            missing_speeds = dataframe.gps_data.isnull()
            for empty, gps_list in zip( list(missing_speeds), list(dataframe.gps_data) ):
                if not empty:
                    speeds.append(gps_list[0][2])
                else:
                    speeds.append(None)
        except AttributeError:
            pass

        try:
            throttles = replace_nans( list(dataframe.lab_throt_per.isnull()), list(dataframe.lab_throt_per) )
        except AttributeError:
            throttles = list()
        try:
            engine_rpms = replace_nans( list(dataframe.lab_engine_rpm.isnull()), list(dataframe.lab_engine_rpm), valid_maximum = 10000 )
        except AttributeError:
            engine_rpms = list()

        try:
            rolls = replace_nans( list(dataframe.imu_roll.isnull()), list(dataframe.imu_roll) )
        except AttributeError:
            rolls = list()

        try:
            pitches = replace_nans( list(dataframe.imu_pitch.isnull()), list(dataframe.imu_pitch) )
        except AttributeError:
            pitches = list()

        four_by_fours = list()
        try:
            current_value = None
            for empty, value in zip( list(dataframe.lab_four_by_four.isnull()), list(dataframe.lab_four_by_four) ):
                if not empty:
                    four_by_fours.append(value)
                    current_value = value
                elif current_value is not None:
                    four_by_fours.append(current_value)
        except AttributeError:
            four_by_fours = list()

        low_gears = list()
        try:
            current_value = None
            for empty, value in zip( list(dataframe.lab_low_gear.isnull()), list(dataframe.lab_low_gear) ):
                if not empty:
                    low_gears.append(value)
                    current_value = value
                elif current_value is not None:
                    low_gears.append(current_value)
        except AttributeError:
            low_gears = list()

        fig = go.Figure()

        fig.add_trace(go.Scatter(
            x=all_times,
            y=throttles,
            name="Throttle"
        ))

        fig.add_trace(go.Scatter(
            x=all_times,
            y=engine_rpms,
            name="Engine RPM",
            yaxis='y2'
        ))

        fig.add_trace(go.Scatter(
            x=all_times,
            y=speeds,
            name="Speed",
            yaxis='y3'
        ))

        fig.add_trace(go.Scatter(
            x=all_times,
            y=four_by_fours,
            name="Four-by-Four",
            yaxis='y4',
            visible='legendonly'
        ))

        fig.add_trace(go.Scatter(
            x=all_times,
            y=low_gears,
            name="Low Gear",
            yaxis='y4',
            visible='legendonly'
        ))

        # Create axes
        fig.update_layout(
            xaxis = dict(
                domain = [0.1, 0.9]
            ),
            yaxis=dict(
                title="Throttle Percent",
                titlefont=dict(
                    color="blue"
                ),
                tickfont=dict(
                    color="blue"
                )
            ),
            yaxis2=dict(
                title="Engine RPMs",
                anchor="x",
                overlaying="y",
                side="right",
                titlefont=dict(
                    color="red"
                ),
                tickfont=dict(
                    color="red"
                )
            ),
            yaxis3=dict(
                title="Speed",
                anchor="free",
                overlaying="y",
                side="left",
                position=0.0,
                titlefont=dict(
                    color="green"
                ),
                tickfont=dict(
                    color="green"
                )
            ),
            yaxis4=dict(
                title="Roll/Pitch",
                anchor="free",
                overlaying="y",
                side="left",
                position=0.1,
                visible = False
            )
        )
        # Update layout properties
        fig.update_layout(paper_bgcolor='#f9f9f9', uirevision=date,
                          margin={'l': 40, 'b': 40, 't': 10, 'r': 100}, 
                          modebar={'orientation': 'v', 'activecolor': '#00a355'},
                          legend={'orientation': 'h'})

        return fig

@app.callback(Output('uv-time-range-store', 'data'),
              [Input('uv-time-range-slider', 'value')])
def store_time_range_as_datetime(time_range):
    return [datetime.fromtimestamp(time_range[0]-FOUR_HOURS), datetime.fromtimestamp(time_range[1]-FOUR_HOURS)]

app.clientside_callback(
    """
    function(time_range, figure, lock_y, graph_selected, date){
        if (time_range != undefined && figure != undefined){
            var start_time     = new Date(time_range[0])
            var end_time       = new Date(time_range[1])
            const layout       = figure['layout'];
            var xaxis_range  = new Object();
            var yaxes_locked = new Object();
            if (graph_selected == 'all_temps') {
                xaxis_range  = { 'xaxis': { 'range': [ start_time, end_time ] } };
                new_yaxis    = {'fixedrange': lock_y};
                yaxes_locked = {
                    'yaxis': {
                        ...layout['yaxis'],
                        ...new_yaxis
                    }
                }
            } else if (graph_selected == 'throttle_rmp_speed'){
                xaxis_range  = { 'xaxis':  { 'range': [ start_time, end_time ], 'domain': [ 0.1, 0.9 ] } }
                new_yaxis    = {'fixedrange': lock_y };
                new_yaxis2   = {'fixedrange': lock_y };
                new_yaxis3   = {'fixedrange': lock_y };
                new_yaxis4   = {'fixedrange': lock_y };
                yaxes_locked = {
                    'yaxis': {
                        ...layout['yaxis'],
                        ...new_yaxis
                    },
                    'yaxis2': {
                        ...layout['yaxis2'],
                        ...new_yaxis2
                    },
                    'yaxis3': {
                        ...layout['yaxis3'],
                        ...new_yaxis3
                    },
                    'yaxis4': {
                        ...layout['yaxis4'],
                        ...new_yaxis4
                    }
                };
            }
            const new_layout = {
                ...layout,
                ...xaxis_range,
                ...yaxes_locked
            };
            return {
                'data': figure['data'],
                'layout': new_layout
            };
        }
    }
    """,
    Output('uv-tab-content', 'figure'),
    [Input('uv-time-range-store', 'data'),
     Input('uv-tab-content-store', 'data'),
     Input('uv-lock-y', 'on')],
    [State('uv-graph-selector', 'value'),
     State('uv-date-dropdown', 'value')])