pythonplotlyipywidgets

Manually adjusting marker position with floatslider


Hej,

I am currently trying to assign markers to data based on user input through ipywidgets. The first user input step, defining offset, Step length, rest length etc. works so far. However, I want to add the option to manually adjust the marker position of Start and End per Stage. I figured out how to get the index of each Start End Marker, but whenever I press run interact, the marker location does not change as well as the marker disappears.

Am I missing something here or why is this happening?

from ipywidgets import Output, VBox, HBox, FloatSlider, Button, interact
import plotly.graph_objs as go
import pandas as pd
output = Output()
markers = {}  # Initialize markers dictionary outside of set_markers function

np.random.seed(0)
df = pd.DataFrame({
    'timeinsec': np.linspace(0, 1000, 100),  # Time from 0 to 1000 seconds
    "data": np.random.rand(100)  # Random values
})


def set_markers(df, x_col, y_col, offset, step_length, rest_length, num_intervals, last_step_length, start_velocity, velocity_increment):
    with output:
        output.clear_output(wait=True)
        
        fig = go.Figure()
        fig.add_trace(go.Scatter(x=df[x_col], y=df[y_col], mode='markers', name='original', marker=dict(color='red', size=8, line=dict(width=1, color='DarkSlateGrey'))))
        
        velocities = [start_velocity + i * velocity_increment for i in range(num_intervals)]
        
        for i in range(num_intervals):
            start = offset + i * (step_length + rest_length)
            end = start + (last_step_length if i == num_intervals - 1 else step_length)
            fig.add_shape(type="line", x0=start, y0=df[y_col].min(), x1=start, y1=df[y_col].max(), line=dict(color="blue", width=2))
            fig.add_shape(type="line", x0=end, y0=df[y_col].min(), x1=end, y1=df[y_col].max(), line=dict(color="blue", width=2,))
            markers[f"Start{i+1}"] = start  # Create start marker positions
            markers[f"End{i+1}"] = end  # Create end marker positions
            
            df.loc[(df[x_col] >= start) & (df[x_col] <= end), 'v'] = velocities[i]
            
            # Create a new marker for the end position of each stage
            fig.add_shape(type="line", x0=end, y0=df[y_col].min(), x1=end, y1=df[y_col].max(), line=dict(color="green", width=2))
        
        # Calculate velocity for the last stage based on proportional length
        if num_intervals > 1:
            stage_per = (last_step_length * 100 / step_length) / 100
            new_v = velocities[-2] + velocity_increment * stage_per
            df.loc[(df[x_col] >= start) & (df[x_col] <= end), 'v'] = new_v

            df.v.fillna(0,inplace = True)
        
        fig.update_layout(title='Assign Stage:', xaxis_title=x_col, yaxis_title=y_col)
        fig.show()

        # Add sliders to adjust the position of the start and end markers for each stage
        sliders_column_1 = VBox()
        sliders_column_2 = VBox()

        for i in range(num_intervals):
            start_marker_pos = markers.get(f"Start{i+1}", 0)  # Get initial position from markers dictionary
            end_marker_pos = markers.get(f"End{i+1}", 0)  # Get initial position from markers dictionary

            start_marker_slider = FloatSlider(value=start_marker_pos, min=0, max=df[x_col].max(), description=f'Start {i+1}:')
            sliders_column_1.children += (start_marker_slider,)  # Add slider to the VBox

            end_marker_slider = FloatSlider(value=end_marker_pos, min=0, max=df[x_col].max(), description=f'End {i+1}:')
            sliders_column_2.children += (end_marker_slider,)  # Add slider to the VBox

        # Arrange sliders in two columns
        sliders_box = HBox([sliders_column_1, sliders_column_2])
        display(sliders_box)

        def update_marker_positions(**kwargs):
            with output:
                output.clear_output(wait=True)
                shapes = []
                for i in range(num_intervals):
                    if f"Start{i+1}" in kwargs and f"End{i+1}" in kwargs:
                        start_marker_pos = kwargs[f"Start{i+1}"]
                        end_marker_pos = kwargs[f"End{i+1}"]
                        markers[f"Start{i+1}"] = start_marker_pos
                        markers[f"End{i+1}"] = end_marker_pos
                        shapes.append(dict(type="line", x0=start_marker_pos, y0=df[y_col].min(), x1=start_marker_pos, y1=df[y_col].max(), line=dict(color="blue", width=2)))
                        shapes.append(dict(type="line", x0=end_marker_pos, y0=df[y_col].min(), x1=end_marker_pos, y1=df[y_col].max(), line=dict(color="green", width=2)))
                fig.update_layout(shapes=shapes)
                fig.show()

        interact_manual(update_marker_positions)


def interactive_line_plot(df, x_col, y_col):
    offset = IntSlider(value=10, min=0, max=df[x_col].max(), step=1, description='Offset:')
    step_length = IntSlider(value=240, min=1, max=500, step=1, description='Step Length:')
    rest_length = IntSlider(value=30, min=1, max=500, step=1, description='Rest Length:')
    num_intervals = IntSlider(value=5, min=1, max=10, step=1, description='Intervals:')
    last_step_length = IntSlider(value=240, min=1, max=500, step=1, description=' t Last Step:')
    start_velocity = FloatText(value=8.0, description='Start:')
    velocity_increment = FloatText(value=1.0, description='Increment:')
    
    update_plot_button = Button(description="Update Plot")
    update_plot_button.on_click(lambda b: set_markers(df, x_col, y_col, offset.value, step_length.value, rest_length.value, num_intervals.value, last_step_length.value, start_velocity.value, velocity_increment.value))
    
    control_box = VBox([offset, step_length, rest_length, num_intervals, last_step_length, start_velocity, velocity_increment, update_plot_button])
    display(HBox([control_box, output]))

interactive_line_plot(df, "timeinsec", "data")

Solution

  • I wasn't seeing kwargs passed into update_marker_positions() passing in anything useful. (Maybe you had different luck?) So every time you check if f"Start{i+1}" in kwargs and f"End{i+1}" in kwargs:, it evaluates to False and nothing happens. This also means you cannot use start_marker_pos = kwargs[f"Start{i+1}"] and end_marker_pos = kwargs[f"End{i+1}" to get the updated values of the sliders.

    I am actually not sure you need any conditional there at the line if f"Start{i+1}" in kwargs and f"End{i+1}" in kwargs: before you update the assignments; however, I've kept it in to stick closer to your original code.

    This below seems to work in my hands for now to give you a chance to refine the starts and ends for each interval. I basically updated how start_marker_pos = kwargs[f"Start{i+1}"] and end_marker_pos = kwargs[f"End{i+1}" get handled:

    from ipywidgets import Output, VBox, HBox, FloatSlider, Button, interact, IntSlider, FloatText, interact_manual
    import numpy as np
    import plotly.graph_objs as go
    import pandas as pd
    output = Output()
    markers = {}  # Initialize markers dictionary outside of set_markers function
    
    np.random.seed(0)
    df = pd.DataFrame({
        'timeinsec': np.linspace(0, 1000, 100),  # Time from 0 to 1000 seconds
        "data": np.random.rand(100)  # Random values
    })
    
    
    def set_markers(df, x_col, y_col, offset, step_length, rest_length, num_intervals, last_step_length, start_velocity, velocity_increment):
        with output:
            output.clear_output(wait=True)
            
            fig = go.Figure()
            fig.add_trace(go.Scatter(x=df[x_col], y=df[y_col], mode='markers', name='original', marker=dict(color='red', size=8, line=dict(width=1, color='DarkSlateGrey'))))
            
            velocities = [start_velocity + i * velocity_increment for i in range(num_intervals)]
            
            for i in range(num_intervals):
                start = offset + i * (step_length + rest_length)
                end = start + (last_step_length if i == num_intervals - 1 else step_length)
                fig.add_shape(type="line", x0=start, y0=df[y_col].min(), x1=start, y1=df[y_col].max(), line=dict(color="blue", width=2))
                fig.add_shape(type="line", x0=end, y0=df[y_col].min(), x1=end, y1=df[y_col].max(), line=dict(color="blue", width=2,))
                markers[f"Start{i+1}"] = start  # Create start marker positions
                markers[f"End{i+1}"] = end  # Create end marker positions
                
                df.loc[(df[x_col] >= start) & (df[x_col] <= end), 'v'] = velocities[i]
                
                # Create a new marker for the end position of each stage
                fig.add_shape(type="line", x0=end, y0=df[y_col].min(), x1=end, y1=df[y_col].max(), line=dict(color="green", width=2))
            
            # Calculate velocity for the last stage based on proportional length
            if num_intervals > 1:
                stage_per = (last_step_length * 100 / step_length) / 100
                new_v = velocities[-2] + velocity_increment * stage_per
                df.loc[(df[x_col] >= start) & (df[x_col] <= end), 'v'] = new_v
    
                df.v.fillna(0,inplace = True)
            
            fig.update_layout(title='Assign Stage:', xaxis_title=x_col, yaxis_title=y_col)
            fig.show()
    
            # Add sliders to adjust the position of the start and end markers for each stage
            sliders_column_1 = VBox()
            sliders_column_2 = VBox()
    
            for i in range(num_intervals):
                start_marker_pos = markers.get(f"Start{i+1}", 0)  # Get initial position from markers dictionary
                end_marker_pos = markers.get(f"End{i+1}", 0)  # Get initial position from markers dictionary
    
                start_marker_slider = FloatSlider(value=start_marker_pos, min=0, max=df[x_col].max(), description=f'Start {i+1}:')
                sliders_column_1.children += (start_marker_slider,)  # Add slider to the VBox
    
                end_marker_slider = FloatSlider(value=end_marker_pos, min=0, max=df[x_col].max(), description=f'End {i+1}:')
                sliders_column_2.children += (end_marker_slider,)  # Add slider to the VBox
    
            # Arrange sliders in two columns
            sliders_box = HBox([sliders_column_1, sliders_column_2])
            display(sliders_box)
    
            def update_marker_positions(**kwargs):
                with output:
                    output.clear_output(wait=True)
                    shapes = []
                    for i in range(num_intervals):
                        if f"Start{i+1}" in markers and f"End{i+1}" in markers:
                            start_marker_pos = list(sliders_column_1.children)[i].value
                            end_marker_pos = list(sliders_column_2.children)[i].value
                            markers[f"Start{i+1}"] = start_marker_pos
                            markers[f"End{i+1}"] = end_marker_pos
                            shapes.append(dict(type="line", x0=start_marker_pos, y0=df[y_col].min(), x1=start_marker_pos, y1=df[y_col].max(), line=dict(color="blue", width=2)))
                            shapes.append(dict(type="line", x0=end_marker_pos, y0=df[y_col].min(), x1=end_marker_pos, y1=df[y_col].max(), line=dict(color="green", width=2)))
                    fig.update_layout(shapes=shapes)
                    fig.show()
    
            interact_manual(update_marker_positions)
    
    
    def interactive_line_plot(df, x_col, y_col):
        offset = IntSlider(value=10, min=0, max=df[x_col].max(), step=1, description='Offset:')
        step_length = IntSlider(value=240, min=1, max=500, step=1, description='Step Length:')
        rest_length = IntSlider(value=30, min=1, max=500, step=1, description='Rest Length:')
        num_intervals = IntSlider(value=5, min=1, max=10, step=1, description='Intervals:')
        last_step_length = IntSlider(value=240, min=1, max=500, step=1, description=' t Last Step:')
        start_velocity = FloatText(value=8.0, description='Start:')
        velocity_increment = FloatText(value=1.0, description='Increment:')
        
        update_plot_button = Button(description="Update Plot")
        update_plot_button.on_click(lambda b: set_markers(df, x_col, y_col, offset.value, step_length.value, rest_length.value, num_intervals.value, last_step_length.value, start_velocity.value, velocity_increment.value))
        
        control_box = VBox([offset, step_length, rest_length, num_intervals, last_step_length, start_velocity, velocity_increment, update_plot_button])
        display(HBox([control_box, output]))
    
    interactive_line_plot(df, "timeinsec", "data")