Parallel coordinates Plot: Selecting just one line

Hi, I’ve created a Parallel Coordinates Plot, and was wondering if it is possible to highlight/brush just one line in the data by selecting the value from a table.

My table is connected to the data that is shown on my PCP, and whenever I click the table I get the value of the row back, I wish to highlight this row in the PCP, is it possible?

FYI

Hi! Thanks for the suggestion, I don’t see how it’s the same that I am doing.

I was hoping it was possible to select a row from the table and then color that line (or all other lines) to highlight that particular row on the PCP.

Just reverse the direction of the callback.
fine. @celia is on the road.

Hi again,
I can’t make the same solution work for me.

The input and format I get from clicking a row is:
{ 'row': 0, 'petal_length': 4.7, 'petal_width': 1.2, 'sepal_length': 6.1, 'sepal_width': 2.8}

So I use this input in my update_parallel_coordinates_plot:

@app.callback(
    Output("parallel-coordinates-plot", "figure"),
    [Input("data_set", "data"),
     Input("get_class_dropdown", "value"),
     Input("row", "data")],
    )
def update_parallel_coordinates_plot(df, class_input, clicked_cell):
    """
    Callback that updates the parallel coordinates plot
    :param df:           dataframe
    :param class_input:  class input
    :param clicked_cell: clicked row from table
    :return:             figure
    """
    df_shap = pd.DataFrame(df).copy()

    ylimits = get_limits(subset_columns(df_shap))
    df = get_df_class(df_shap, class_input)

    fig = go.Figure(data=go.Parcoords(
        line=dict(color=df['y_pred']),
        dimensions=list(get_dimensions(subset_columns(df), ylimits))))

    fig.update_traces(labelangle=45)

    # If clicked:
        # then update the figure giving that one point a particular color

    return fig

But I simply cannot figure out how to change the color of that particular row. I was thinking of adding the state of the figure, and using the:

'line': {'color': [1, 1, 1, 1, 2, 1, 1, 2, 1, 2, 1, 1, 2, 2, 2, 2, 2, 2, 1, 2, 1, 2, 1, 1, 1, 1, 2, 1, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]}, 'type': 'parcoords'}],

and select the item in the array corresponding to the row value and change the color that way. But I have not had success with that approach yet.

Also, I’m not sure what this means? :smiley:

@celia is on the road.

:point_up:She always has the perfect solution.

from dash import Dash, html, dcc
from dash.dash_table import DataTable
from dash.dependencies import Output, Input, State
from dash.exceptions import PreventUpdate
import plotly.express as px

df = px.data.iris()
fig = px.parallel_coordinates(
    df,
    color="species_id",
    labels={
        "species_id": "Species",
        "sepal_width": "Sepal Width",
        "sepal_length": "Sepal Length",
        "petal_width": "Petal Width",
        "petal_length": "Petal Length",
    },
    color_continuous_scale=px.colors.diverging.Tealrose,
    color_continuous_midpoint=2)

app = Dash(__name__)

app.layout = html.Div([
    my_graph := dcc.Graph(figure=fig), my_table :=
    DataTable(df.to_dict('records'), [{
        "name": i,
        "id": i
    } for i in df.columns],
              row_selectable='single')
])


@app.callback(Output(my_graph, 'figure'), Input(my_table, 'selected_rows'),
              State(my_graph, 'figure'))
def pick(r, f):
    if r is None:
        raise PreventUpdate

    row = df[[
        "sepal_length", "sepal_width", "petal_length", "petal_width",
        "species_id"
    ]].loc[r[0]].to_list()

    for i, v in enumerate(f.get('data')[0].get('dimensions')):
        v.update({'constraintrange': [row[i] - row[i] / 100000, row[i]]})

    return f


if __name__ == "__main__":
    app.run_server(debug=True)
from dash import Dash, html, dcc
from dash.dash_table import DataTable
from dash.dependencies import Output, Input, State
from dash.exceptions import PreventUpdate
import plotly.express as px

df = px.data.iris()

app = Dash(__name__)

app.layout = html.Div([
    my_graph := html.Div(), my_table := DataTable(df.to_dict('records'),
                                                  [{
                                                      "name": i,
                                                      "id": i
                                                  } for i in df.columns],
                                                  row_selectable='single')
])


@app.callback(Output(my_graph, 'children'), Input(my_table, 'selected_rows'))
def pick(r):

    labels = {
        "species_id": "Species",
        "sepal_width": "Sepal Width",
        "sepal_length": "Sepal Length",
        "petal_width": "Petal Width",
        "petal_length": "Petal Length",
    }

    fig = px.parallel_coordinates(
        df,
        color="species_id",
        labels=labels,
        color_continuous_scale=px.colors.diverging.Tealrose,
        color_continuous_midpoint=2)

    if r is not None:
        row = df[[
            "sepal_length", "sepal_width", "petal_length", "petal_width",
            "species_id"
        ]].loc[r[0]]
        fig.update_traces(dimensions=list([
            dict(constraintrange=[j - j / 100000, j],
                 label=labels.get(i),
                 values=df[i]) for i, j in row.to_dict().items()
        ]),
                          selector=dict(type='parcoords'))

    return dcc.Graph(figure=fig)


if __name__ == "__main__":
    app.run_server(debug=True)

OMG, the docs has told me <=

The domain range to which the filter on the dimension is constrained. Must be an array of [fromValue, toValue] with fromValue <= toValue, or if multiselect is not disabled, you may give an array of arrays, where each inner array is [fromValue, toValue].

1 Like

Thanks for the suggestion! Tried running your example but getting this error:

dash.exceptions.IncorrectTypeException: component_id must be a string or dict, found Graph(figure=Figure({
‘data’: [{‘dimensions’: [{‘label’: ‘Sepal Length’,…

Ah, is your python version over 3.8? And Dash over 2.3?

Yarp, Python 3.9 but dash==2.0.0. :thinking:

@stu Got your example working - thanks a ton!
Here’s how I just tweaked it for the different version:

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


df = px.data.iris()
fig = px.parallel_coordinates(
    df,
    color="species_id",
    labels={
        "species_id": "Species",
        "sepal_width": "Sepal Width",
        "sepal_length": "Sepal Length",
        "petal_width": "Petal Width",
        "petal_length": "Petal Length",
    },
    color_continuous_scale=px.colors.diverging.Tealrose,
    color_continuous_midpoint=2)

app = Dash(__name__)

app.layout = html.Div([
    dbc.Row(dcc.Graph(id="my_graph", figure=fig)),
    html.Div(dash_table.DataTable(data=df.to_dict('records'), row_selectable='single',
                                  columns=[{"name": i, "id": i} for i in df.columns],
                                  id="my_table")),
])


@app.callback(Output("my_graph", 'figure'),
              Input("my_table", 'selected_rows'),
              [State("my_graph", 'figure')])
def pick(r, f):
    print(r)
    print(f)
    if r is None:
        raise PreventUpdate

    row = df[[
        "sepal_length", "sepal_width", "petal_length", "petal_width",
        "species_id"
    ]].loc[r[0]].to_list()

    for i, v in enumerate(f.get('data')[0].get('dimensions')):
        v.update({'constraintrange': [row[i] - row[i] / 100000, row[i]]})

    return f


if __name__ == "__main__":
    app.run_server(debug=True)

Now I will try and apply it to my other project. Cheers again!

1 Like

You can also try changing it to multi-select.

1 Like

Aye, good idea! I will try that next.

Also if I want to increase the thickness of the line, what can I add to the update to do so? :slight_smile:

I don’t know. Maybe you could find something out in this below.

Thanks @stu, it seems like thickness is for another time, there seems not to be anything for PCPs yet.

Could I ask you regarding highlighting several lines? I’ve changed my table to multi-select, and I’ve taken a look at the constraintrange from the docs where it says:

And I have been attempting to apply this to my code, but it just seems to select the next line (so going from line A to line B, instead of highlighting both line A and line B).

So I thought this change would be sufficient:

    if clicked_row is not None and clicked_row != []:
        rows = []
        save_constraints = []
        prev_len = len(clicked_row) - 1
        current_len = len(clicked_row)

        if prev_len < current_len:
            for i in clicked_row:
                rows.append(subset_columns(df).loc[clicked_row[prev_len ]].to_list())

            for j, val in enumerate(rows):
                for i, v in enumerate(fig['data'][0]['dimensions']):
                    save_constraints.append([rows[j][i] - rows[j][i] / 100000, rows[j][i]])

            for k, val in enumerate(fig['data'][0]['dimensions']):
                val.update({'constraintrange': [save_constraints[k][0], save_constraints[k][1]]})

But, unfortunately not, its simply changes the highlighted constraints. Any clue how to implements multi-constraints?

You may consider taking a detailed look at the fig object returned by the front end.

Great post, thanks for sharing! I really like how you used the plot to show the relationship between the variables. I wonder if it would be useful to filter the table by the lines selected in the plot? That way, you could see the exact values for each group and compare them more easily. Just a suggestion, feel free to ignore it if you think it’s not relevant.