Skip to content

Events

Overview

The Plot.events mark supports mouse interactions via the following callbacks: onDrawStart, onDraw, onDrawEnd, onClick, and onMouseMove.

Each callback receives an event object containing type (the event name), x, y, and startTime (for draw events only, to distinguish one draw event from another).

(
    Plot.initialState({"points": []})
    # setting $state.points in the `onDraw` callback,
    # which is passed an event containing a `point`, an `[x, y]` array.
    + Plot.events(
        onDrawStart=js("(event) => $state.points = [[event.x, event.y]]"),
        onDraw=js("(event) => $state.points = [...$state.points, [event.x, event.y]]"),
    )
    # Draw a line through all points
    + Plot.line(js("$state.points"), stroke="blue", strokeWidth=4)
    + Plot.ellipse([[1, 1]], r=1, opacity=0.5, fill="red")
    + Plot.domain([0, 2])
)

Drawing Example

interactivity_warning

Say we wanted to pass a drawn path back to Python. We can initialize a ref, with an initial value of an empty list, to hold drawn points. Then, we pass in a python onDraw callback to update the points using the widget's state.update method. This time, let's add some additional dot marks to make our line more interesting.

(
    Plot.initialState(
        {"all_points": [], "drawn_points": [], "clicked_points": []}, sync=True
    )
    # Create drawing area and update points on draw
    | Plot.events(
        onDraw=js(
            "(e) => $state.update(['drawn_points', 'append', [e.x, e.y, e.startTime]])"
        ),
        onMouseMove=js("(e) => $state.update(['all_points', 'append', [e.x, e.y]])"),
        onClick=js("(e) => $state.update(['clicked_points', 'append', [e.x, e.y]])"),
    )
    # Draw a continuous line through drawn points
    + Plot.line(js("$state.drawn_points"), z="2")
    # Add small dots for drawn points
    + Plot.dot(js("$state.drawn_points"))
    # Highlight every 6th drawn point in red
    + Plot.dot(
        js("$state.drawn_points"),
        Plot.select(
            js("(indexes) => indexes.filter(i => i % 6 === 0)"),
            {"fill": "red", "r": 10},
        ),
    )
    # Add symbol for clicked points
    + Plot.dot(js("$state.clicked_points"), r=10, symbol="star")
    # Add light gray line for all points
    + Plot.line(js("$state.all_points"), stroke="rgba(0, 0, 0, 0.2)")
    + Plot.domain([0, 2])
    | [
        "div.bg-blue-500.text-white.p-3.rounded-sm",
        {"onClick": lambda widget, e: print(widget.state.clicked_points)},
        "Print clicked points",
    ]
    | [
        "div.bg-blue-500.text-white.p-3.rounded-sm",
        {
            "onClick": lambda widget, e: widget.state.update(
                {"all_points": [], "drawn_points": []}
            )
        },
        "Clear Line",
    ]
) | Plot.onChange({"clicked_points": print})

The onDraw callback function updates the points state with the newly drawn path. This triggers a re-render of the plot, immediately reflecting the user's drawing.

Child Events

interactivity_warning

This example demonstrates how to create an interactive scatter plot with draggable points. We will use Plot.renderChildEvents, a render transform. It handles click and drag events for any mark which produces an ordered list of svg elements, such as Plot.dot.

We first define a reference with initial point coordinates to represent the points that we want to interact with.

Next we define a callback function, which will receive mouse events from our plot. Each event will contain information about the child that triggered the event, as well as a reference to the current widget, which has a .state.update method. This is what allows us to modify the plot in response to user actions.

When creating the plot, we pass Plot.render.childEvents as a render option to the Plot.dot mark. For demonstration purposes we also include a Plot.ellipse mark behind the interactive dots. The Plot.dot mark updates immediately in JavaScript, while the Plot.Ellipse mark updates only in response to our callback.

(
    Plot.ellipse(data, {"fill": "cyan", "fillOpacity": 0.5, "r": 0.2})
    + Plot.dot(
        data,
        render=Plot.renderChildEvents(
            {"onDrag": update_position, "onDragEnd": print, "onClick": print}
        ),
    )
    + Plot.domain([0, 2])
    + Plot.aspectRatio(1)
).display_as("widget")