Search code examples
pythononclickplotlystacked-bar-chart

Clicking on one plot opens another in plotly


I've a stacked bar plot(as shown in below diagram) plotted using plotly in python

enter image description here

What I want is whenever I click on any particular bar of any stacked bar, it show a corresponding line plot. eg :- Let suppose I click on A's bar then it should show the below line plot separately.

enter image description here


Solution

  • There are three ways that I know of to do this.

    1. use dash, like this example
    2. use a notebook, like this example
    3. use plotly.io's argument post_script to add Javascript (and write the code to change the plot this way in Javascript)

    I couldn't find any examples of a drill down in using traditional Python & Plotly (I found that really surprising).

    It looks like you’re pretty new to SO, welcome to the community. To get great answers quickly, it’s best to make your question reproducible. This includes things like the data you used. If you used a pandas data frame, extract the data with pd.DataFrame.to_dict and copy the output exactly as it appears. If you used base Python data structures, try repr (no import needed).

    I've put together an example of how to use option 3.

    enter image description here

    enter image description here

    I've added comments to the JS (nested in /* comment */) so that you could see/follow what's happening. If anything is unclear, let me know. Additionally, I added the JS separately as JS, without all of the ancillary things like "" & + that are necessary when embedding it in Python.

    There are probably more comments in the JS than JS code.

    Explanation:: Events in JS

    There are 2 events in the JS: click and double-click. The click event has 3 conditions.

    To use the click event you click on a plotted element. In the line chart, that means you would have to find a point that is plotted within the line, that's why I added a double-click event. That way you can click anywhere on the plot to return to the parent (the bar graph).

    I tend to have problems triggering double-click events in Plotly without adding a delay. Therefore, in the call to show the plot, I use config to add a click delay to the double-click event.

    In the click event

    There is a condition for each item in the legend, and one for the line graphs.

    You have several categories in your chart, whereas I only have 2. If your real data has many categories, I can help you write this as a loop, instead of the lengthy process of writing if/else for each category. Let me know if this is the case.

    In each condition, the visibility changes, the type changes (line or bar), and an annotation changes (annotation only appears when showing the line graph).

    In the comments, you'll see UPDATE where you may need to make changes. Where the condition is "a" == ty, "b" == ty, and within every condition, where you see visible. a, and b are my legend entries. Visibility is in reference to the number of traces that are rendered (not the number of traces you code in Python). Each color is a separate trace. You need either true or false in these lists for each trace. Assume that the order in the legend is the order the traces are documented in Python. For example, if you have five traces, you need five boolean values in each list indicated with visible.

    In the double-click event

    The double-click event is strictly for returning to the parent (the bar graph). The click event also contains a condition for the line graph. This code is nearly identical. The only difference is that there are no conditions in the double-click event.

    The Code

    The imports, data I used, the Javascript, and the plot. I used Plotly Express. However, it doesn't matter if you use Plotly Graph Objects or Plotly Express.

    As I mentioned earlier, in the JS there are a few things you will need to change. These are annotated with JS comments, these comments stick out further to the right than all others, and include the word UPDATE (in all caps).

    import plotly.express as px
    import plotly.io as io
    import pandas as pd
    import datetime as dt
    
    
    df1 = pd.DataFrame({ # create data for example
        'x': [dt.datetime.today() - dt.timedelta(days = x) for x in range(5)] + 
                [dt.datetime.today() - dt.timedelta(days = x) for x in range(5)],
        'y': [10, 0, 20, 10, 5, 10, 25, 3, 14, 30],
        'clr': ['a'] * 5 + ['b'] * 5 
    })
    
    fig = px.bar(df1, x = "x", y = "y", color = "clr") # plot the bar chart
    
    # note that the double click and single click event can return the plot to the original state
    # in the single click event, the user would have to click on a literal data point within the line
    ps = 'setTimeout(function() {\n' + \
         '  myplt = document.querySelector("#plt");\n' + \
         '  myplt.on("plotly_click", function(dt) {                         /* EVENT 1: CLICK */\n' + \
         '      nm = dt.points[0].data.name;\n' + \
         '      ty = dt.points[0].data.type;\n' + \
         '      lout = {                                       /* used in multiple conditions */\n' + \
         '          annotations: [{\n' + \
         '              xref: "paper", yref: "paper", x: 1, y: 1, showarrow: false,\n' + \
         '              text: "Double click the chart to return to the bar chart" \n' + \
         '          }]\n' + \
         '      };\n' + \
         '      if(ty != "bar") {                      /* bar or line type? UPDATE HERE: add true for each color */\n' + \
         '          trc = {type: "bar", visible: [true, true]};\n' + \
         '          lout = {annotations: []};                             /* remove click note */\n' + \
         '          Plotly.update("plt", trc, lout);                /* return to original plot */\n' + \
         '      } else if(nm == "a") {                /* UPDATE HERE: replace "a" with a category from your data */\n' + \
         '                     /* make only one trace invisible, change the type to line graph */\n' + \
         '          trc = {type: "scatter", mode: "lines", visible: [true, false]}; /* UPDATE HERE: add booleans */\n' + \
         '          Plotly.update("plt", trc, lout);\n' + \
         '      } else if(nm == "b") {                /* UPDATE HERE: replace "a" with a category from your data */\n' + \
         '                     /* make only one trace invisible, change the type to line graph */\n' + \
         '          trc = {type: "scatter", mode: "lines", visible: [false, true]}; /* UPDATE HERE: add booleans */\n' + \
         '          Plotly.update("plt", trc, lout);\n' + \
         '      }\n' + \
         '  });\n' + \
         '  myplt.on("plotly_doubleclick", function() {                 /* EVENT 2: DOUBLECLICK */\n' + \
         '                                                              /* UPDATE HERE: add true for each color */\n' + \
         '      trc = {type: "bar", visible: [true, true]};\n' + \
         '      lout = {annotations: []};                                  /* remove click note */\n' + \
         '      Plotly.update("plt", trc, lout);                     /* return to original plot */\n' + \
         '  });\n' + \
         '}, 200);\n'
    
    io.write_html( # untitled.html created, but plot opens in browser, as well
        fig, file = "untitled.html", post_script = ps, auto_open = True, 
        config = [{'doubleClickDelay': 750}], div_id = "plt",
        include_plotlyjs = 'cdn', include_mathjax = 'cdn')
    

    Just the Javascript

    ...as it will appear behind the scenes in your browser when you render the plot.

    setTimeout(function() {
      myplt = document.querySelector("#plt");
      myplt.on("plotly_click", function(dt) {
          nm = dt.points[0].data.name;
          ty = dt.points[0].data.type;
          lout = {                                        /* layout update; used in each line graph */
              annotations: [{
                  xref: "paper", yref: "paper", x: 1, y: 1, showarrow: false,
                  text: "Double click the chart to return to the bar chart" 
              }]
          };
          if(ty != "bar") {                                /* is this a drilldown, a line graph? */
                            /* trace updates, add -true- for each legend item (each color) */
              trc = {type: "bar", visible: [true, true]};
              lout = {annotations: []};                                      /* remove annotation */
              Plotly.update("plt", trc, lout);              /* return to original plot */
          } else if(nm == "a") {           /* NOTE: replace "a" with your data (note a in legend) */
              trc = {type: "scatter", mode: "lines", visible: [true, false]};
              Plotly.update("plt", trc, lout);        /* create single trace line graph of ONLY a */
          } else if(nm == "b") {
              trc = {type: "scatter", mode: "lines", visible: [false, true]};
              Plotly.update("plt", trc, lout);        /* create single trace line graph of ONLY b */
          }
      });
      myplt.on("plotly_doubleclick", function() {        /* alternate to finding a pt within line */
                /* trace updates, add -true- for each legend item (each color) */
          trc = {type: "bar", visible: [true, true]};
          lout = {annotations: []};                                          /* remove annotation */
          Plotly.update("plt", trc, lout);                  /* return to original plot */
      });
    }, 200);