How to add new data point to Graph/canvas by single mouse click?

Dear community,

Newbie Dash/Plotly user here, so please excuse if this post is not complying with community culture or duplicates existing discussions. I have been searching internet and forums but could not find a definite answer.

  • Is it possible to add new data points to Graph/canvas by single mouse click?

I am able to get mouse click coordinate information using figure drawing tools through relayoutData for drawrect, drawcircle etc. But all of these tools seem to require click and drag, which is not per end-user requirements.

Other mouse-click interactions with Graph/canvas seem to be limited to getting coordinates of existing data points only.

Kind regards,
Erlend

Hi @eoian welcome to the forums.

Not sure if I understand that correctly, but you can get the click coordinates without having to drag. Here an example:

You can find an other example where annotations are created by clicking into a graph.

Thanks a lot for the reply and the links/example. This is really useful.

Specifically for the original question on “how to add new data point”, the context is a domain expert end-user needing the capability to add new data points to a scatter plot of existing data.

If I understand plotly correct, I think a key challenge in my case is that “Input(‘graph’, ‘clickData’)” only contains coordinates of existing data, it does not contain the mouse-coordinates if clicking on an empty canvas.

Using " Scatter plots in Dash" as an example, from Scatter plots in Python, the end-user needs to interact with the scatter plot and add new points such that the original dataframe df = px.data.iris() will contain more data/rows after interaction with the plot, i.e. len(df_after_adding_points) > len(df). (btw, a subsequent need is capability to select and move existing data points in the scatter plot).

Is it perhaps possible combine a dcc.Graph with scatter plot on top of a 100% transparent trace with imshow, such that the canvas is completely “filled” with datapoints for Input(‘graph’, ‘clickData’)?

Hello @eoian, that’s a cool thought.

Have it to where the click data adjusts the z value in the data.

I think that this should be possible. Obviously, you’d be confined to the area of the imshow that you render.

Id be careful about generating too many points, you might run into performance issues.

The transparency idea seems to work, but it will be a challenge I guess to transform all existing scatter plot data point values to the underlying image’s 2d space dimensions, and maintaining the scatter point’s original hover-info correct for domain specialist(?)

This is a quck-n-dirty addition to @AMIPED’s nice plotlyDash_annotations.py example (x, y values for scatterplot are not tuned to match img):

# create image and plotly express object
#img = np.random.randint(0, 255, (90, 160))
#fig = px.imshow(img, color_continuous_scale='Blugrn')

img = np.zeros((400, 600, 4), dtype=np.uint8) + 255
img[:, :, 3] = 50
fig = px.imshow(img)

# create simple scatterplot using a random dataset of 2x8 data points, add trace to figure
fig.add_scatter(x=np.random.rand(8)*160, y=np.random.rand(8)*160, mode='markers')

all existing scatter plot data point values to the underlying image's 2d space dimensions, and maintaining the scatter point's original hover-info correct for domain specialist(?)

What do you mean by that?

I think it means that the scatter plot translates to the imshow and vice Versa.

You could also have an edit mode of the chart where it shows the imshow over the scatter, and then you turn off to show the original data.

I see. I thought the imshow was just for adding new scatter points via click coordinates.

@jinnyzor yes, that’s what I was thinking about: the imshow basically needs to be representing the screen’s pixel resolution to maximize accuracy, while the existing scatter data that end-user needs to add new data points to could e.g have x-values [0, 1] and y-values [-10, 100].

@AIMPED, sorry for not framing the question properly, your input is highly appreciated (balance between minimizing vs swamping the original question with details :wink: ).

Ideally there would have been a callback along the lines “Input(‘graph’, ‘clickCanvas’)”, where a single mouse click would return coordinate values of the location clicked in the canvas (not existing data points), but with the returned coordinate values are scaled (translated) to the lie within the axis limits of the graph.

I understand. This is interesting! This is the solution I had in mind. If you do the callback clientside this could work quite well IMHO. But there is still room for improvement I guess.

import json

from dash import Dash, dcc, html, Input, Output, State
import dash_bootstrap_components as dbc
from dash.exceptions import PreventUpdate
import plotly.express as px
import numpy as np

# create image and plotly express object
fig = px.imshow(
    np.zeros(shape=(90, 160, 4))
)
fig.add_scatter(
    x=[5, 20, 50],
    y=[5, 20, 50],
    mode='markers',
    marker_color='white',
    marker_size=10
)

# update layout
fig.update_layout(
    template='plotly_dark',
    plot_bgcolor='rgba(0, 0, 0, 0)',
    paper_bgcolor='rgba(0, 0, 0, 0)',
    width=700,
    height=500,
    margin={
        'l': 0,
        'r': 0,
        't': 20,
        'b': 0,
    }
)

# hide color bar
fig.update_coloraxes(showscale=False)

# Build App
app = Dash(
    __name__,
    external_stylesheets=[dbc.themes.SLATE],
    meta_tags=[
        {
            'name': 'viewport',
            'content': 'width=device-width, initial-scale=1.0'
        }
    ]
)

# app layout
app.layout = dbc.Container(
    [
        dbc.Row(
            dbc.Col(
                dcc.Graph(
                    id='graph',
                    figure=fig,
                    config={
                        'scrollZoom': True,
                        'displayModeBar': False,
                    }
                ),
                width={'size': 5, 'offset': 0}
            ), justify='around'
        ),
        dbc.Row(
            [
                dbc.Col(
                    [
                        html.A(
                            html.Button(
                                'Refresh Page',
                                id='refresh_button'
                            ),
                            href='/'
                        ),
                    ], width={'size': 5, 'offset': 0}
                ),
            ], justify='around'
        )
    ], fluid=True
)


@ app.callback(
    Output('graph', 'figure'),
    State('graph', 'figure'),
    Input('graph', 'clickData')
)
def get_click(graph_figure, clickData):
    if not clickData:
        raise PreventUpdate
    else:
        points = clickData.get('points')[0]
        x = points.get('x')
        y = points.get('y')

        # get scatter trace (in this case it's the last trace)
        scatter_x, scatter_y = [graph_figure['data'][1].get(coords) for coords in ['x', 'y']]
        scatter_x.append(x)
        scatter_y.append(y)

        # update figure data (in this case it's the last trace)
        graph_figure['data'][1].update(x=scatter_x)
        graph_figure['data'][1].update(y=scatter_y)

    return graph_figure


if __name__ == '__main__':
    app.run_server(debug=True, port=8053)

scatter

1 Like

Thanks for providing code examples again. This goes a long way towards a working solution :+1:

I realize that a challenge with domain specific data may start to creep in, see screenshot below:

  1. x-values needs to lie within range [0, 1] → conflict with smallest possible resolution in imshow is the array indices, 0, 1, …
  2. imshow origin location top left(?) needs some kind of axis flip and custom hovertemplate to not confuse end-user when hovering over original and new data points solved by ```origin=‘lower’ as argument to imshow
  3. modebar with possibility to zoom, pan the graph also needs to be active, which I guess means some kind of callback to update the imshow image
  4. Realistic y-values can be negative, challenge with imshow “living” in index-based positive integer space
# create image and plotly express object
fig = px.imshow(
    np.zeros(shape=(1, 10, 4)),
    origin='lower', # flipping vertical axis to align with scatter plot data
)

x = np.array([0.1, 0.2, 0.5, 0.9]) # realistic data range
y = np.array([10, 4, 0.1, -2.5])

# commented out 4 lines below not needed since imshow has origin='lower' argument
# y_original=np.array([10, 4, 0.1, -2.5])
# y_max = 20 # set some arbitrary max value of y axis to test reversal
# # y values needs to be reversed to match the imshow since origin is top left(?)
# y = y_max - y_original

fig.add_scatter(
    x = x,
    y = y,
    mode='markers', 
    marker_color='#1f77b4',
    marker_size=10
) 

Screenshot of data adjustment above, everything in red color is added manually in drawing program to illustrate real-life setting (and the nice dark theme, slate template only worked for 2-3 code re-launches, then code-insiders and/or Chrome / Murphy’s law insisted on plotting all white)