Part 2 of 3: Plotly in React – Display Multiple Selections with Annotations

How to give the user selection capabilities by drawing boxes and polygons in plotly, along with allowing them to persist multiple selections on the plot and having those selections labeled with annotations

In this article…

In this article, I will be showing you how to use plotly’s selection capabilities to draw multiple selections to the plot, along with having those selections individually labelled with plotly’s annotations.

What do you mean by multiple selections with annotations?

Plotly’s out-of-the-box Plot allows a user to make a selection for any chart that involves (x,y) coordinates, but that selection disappears after a user releases the mouse. It takes a bit of extra work, which is what this article will go into, to allow multiple selections to be displayed.

Annotations in plotly are labels that can be layered onto the plot. Since we’ll be displaying multiple selections, we’ll need some sort of label affixed to the polygonal selection to differentiate them from each other.

What you should know beforehand
  • Part 1 of this 3 part series
  • Good understanding of Javascript
  • Basic understanding of Typescript (this app will be in Typescript!)
  • Intermediate understanding of React and React hooks

  • Steps in creating this multiselection data plot
  • Create app in codesandbox.io from app in Part 1
  • Remove useEffect statement and code cleanup
  • Change Plot.data type and seed with random data
  • Create selection state hook and populate it with selection event
  • Pass selections to Plot.layout.shapes
  • Display selection details as text below Plot
  • Create Undo Last Selection button
  • Create Clear All Selections button
  • Create getAnnotationsForSelections function
  • Create getHighestPoint function
  • Populate Plot.layout.annotations with values from getAnnotationsForSelections
  • Create new sandbox environment in codesandbox.io from sandbox in Part 1 (Plotly in React – Real time data visualization)

    Open the app created in Part 1 and click the Fork button at the top right. This will create a new, distinct app that is a copy of your old one.

    Alternatively, you could create a new app in codesandbox.io using the steps given in the article from Part 1, and simply copy and paste the code in App.tsx.

    Remove useEffect statement and code cleanup

    There is a useEffect statement and a single react hook in App.tsx that are both no longer needed. You can safely remove both and your App.tsx should now look like this:

    export default function App() {
     return (
       <div className="App">
         <Plot
           data={[data]}
           layout={{
             title: "Real-time Data App",
             xaxis: { range: [-5, count] },
             yaxis: { range: [-5, count] }
           }}
         />
       </div>
     );
    }
    
    Note: this will break the app as the `data` variable is no longer defined. We’ll fix this in the following step.

    Change Plot.data type and seed with random data

    Above the line where you declare App.tsx, create a new function which returns an array of `count` length, with random numbers populated inside:

    const randomNumbers = Array(count)
     .fill(1)
     .map((_, i) => Math.floor(Math.random() * count));
    
    Then inside your Plot element, change the Plot props to what is show below:
           data={[
             {
               x: startingNumbers,
               y: randomNumbers,
               mode: "markers"
             }
           ]}
           layout={{
             title: "Multi Selection App",
             xaxis: { range: [-5, count] },
             yaxis: { range: [-5, count] }
           }}
         />
    
    Explanation of changes:

  • We are no longer dynamically updating with random data, but instead passing in hardcoded data that is randomized from the start.
  • We set the data mode to “markers”, which shows individual points instead of lines
  • – Learn more about plotly mode here

  • We changed the title to Multi Selection App.
  • Your app should now look something like this:

    Note: with this new mode, we also get some extra buttons in the top right. Hover over each one to familiarize yourself with the, particularly the selection buttons “Box Select” and “Lasso Select”

    Create selection state hook and populate it with selection event

    Create a new file in the same directory as App.tsx and call it “types.ts”

    In “types.ts”, paste in the following code below. We’ll be adding more types to this file in future steps.
    import { PlotSelectionEvent, Shape } from "plotly.js";
     
    export type PlotSelectionState = PlotSelectionEvent & {
     selections?: Partial<Shape>[];
    };
     
    
    Back in App.tsx, add your state hook that will contain your plot selections:
    export default function App() {
     const [allSelections, setAllSelections] = React.useState<
       PlotSelectionState[]
     >([]);
    
    Create a new function that will be passed to the onSelected event in the Plot component.
     const setPointsAsSelected = (cs?: PlotSelectionState) => {
       if (cs && cs.points?.length > 0) setAllSelections((curr) => [...curr, cs]);
     };
     
     return (
       <div className="App">
         <Plot
           onSelected={setPointsAsSelected}
           …
    
    Explanation of changes:

  • Plot.onSelected simply takes the current selection–which is an object–and passes that object to setPointsAsSelected, which then appends it to our allSelections array.
  • Why create the type PlotSelectionState as an extension of PlotSelectionEvent instead of just using PlotSelectionEvent itself? This is an unusual case where the type specified in the plotly source code doesn’t fully reflect the properties actually passed in from onSelected. If you run a console.log on the `cs` parameter passed to setPointsAsSelected, you’ll see a ‘selections’ property which contains the selections we’ll need in the next step.
  • Note: Plotly openly admits that there are some circumstances where they break React rules and modify props: https://github.com/plotly/react-plotly.js/#basic-props. It’s just something to live with for the moment.

    Pass selections to Plot.layout.shapes

    In Plot.layout, update the properties to match what is shown here:

          layout={{
             title: "Multi Selection App",
             xaxis: { range: [-5, count] },
             yaxis: { range: [-5, count] },
             dragmode: "lasso",
             uirevision: 1,
             shapes: allSelections
               ? allSelections.flatMap((x) =>
                   x.selections
                     ? { ...x.selections[0], line: { dash: "solid" } }
                     : []
                 )
               : undefined
           }}
    
    Explanation of changes:

  • Shapes is a layer in plotly that allows a chart to draw arbitrary shapes likes lines, circles, rectangles, and paths. We’re use our state hook allSelections, and spread the selections properties (which are of type Shape) back into the Plot.layout.shapes prop.
  • Setting the uirevision prop (and not changing it) causes the current interactive state of the plot to persist.
  • The dragmode prop is the default drag mode for when the plot loads. We want it to default to the Lasso selection.
  • Note: You should now notice that lasso or box selections persist in your app!

    Display selection details as text below Plot

    Below your Plot component, create a new component with the following code:

         <div style={{display:'flex'}}>
           {allSelections.map((x, i) => (
             <div style={{marginRight: '10px'}}>
               Selection {i + 1}:
               <ul>{x && x.points.map((p) => <li>{`(${p.x},${p.y})`}</li>)}</ul>
             </div>
           ))}
         </div>
    
    Explanation of changes:

  • This will create an area where you can see all the coordinates within each selection
  • Create Undo Last Selection button

    Above your Plot component, create a button that will slice off the selection object at the end of your allSelections array like this:

         <button onClick={() => setAllSelections((curr) => curr.slice(0, -1))}>
           Undo Last Selection
         </button>
    

    Create Clear All Selections button

    In addition to the button above, create another button that will clear all selections

    Create getAnnotationsForSelections function

    The remaining two steps for this article will deal with adding annotations for each label so we can know which polygon is associated with which selection.

    First, add a new type in the `types.ts` file called Coord:

    export type Coord = { x?: number; y?: number };
    Next, create a new file called getAnnotationsForSelections and populate it with the following code:
    import { Coord, PlotSelectionState } from "./types";
    import { Annotations } from "plotly.js";
     
    export function getAnnotationsForSelections(
     allSelections: PlotSelectionState[]
    ): Partial<Annotations>[] {
     const annotations = allSelections.flatMap((selection, i) => {
       if (!selection) return [];
     
       const { lassoPoints, range } = selection;
     
       let highestPoint: Coord;
     
       if (lassoPoints) {
         const highestYCoord = Math.max(...lassoPoints.y);
         const i = lassoPoints.y.findIndex((y) => y === highestYCoord);
         highestPoint = { x: lassoPoints.x[i], y: lassoPoints.y[i] };
       } else {
         highestPoint = { x: range?.x[0], y: range?.y[1] };
       }
     
       return {
         x: highestPoint?.x,
         y: highestPoint?.y,
         text: `Selection ${i + 1}`
       } as Partial<Annotations>;
     });
     
     return annotations;
    }
    
    Explanation of code:

  • Given the array of selections we want to return an array of annotations which will then be applied to the annotation layer in our Plot.
  • We want to affix the annotation to the highest point of the given selection. Because there are two possible selection shapes–box and lasso–we need two different methods for retrieving the coordinates of the highest point.
  • Populate Plot.layout.annotations with values from getAnnotationsForSelections

    Finally, back in App.tsx set the Plot.layout.annotations property with the return value of the function:

           layout={{
             …
             annotations: getAnnotationsForSelections(allSelections),
             …
    
    And your selections will be annotated with labels!

    Conclusion

    In the next and final article (Part 3 of 3), we’ll look at how we can merge overlapping selections.

    3-Part Series:

    • Part 1 of 3: Plotly in React – Real time data visualization
    • Part 2 of 3: Plotly in React – Display Multiple Selections with Annotations
    • Part 3 of 3: Part 3 of 3: Plotly in React – Merge Multiple Selections using polygon-clipping

    About Intertech

    Intertech is a Software Development Consulting Firm that provides single and multiple turnkey software development teams, available on your schedule and configured to achieve success as defined by your requirements independently or in co-development with your team. Intertech teams combine proven full-stack, DevOps, Agile-experienced lead consultants with Delivery Management, User Experience, Software Development, and QA experts in Business Process Automation (BPA), Microservices, Client- and Server-Side Web Frameworks of multiple technologies, Custom Portal and Dashboard development, Cloud Integration and Migration (Azure and AWS), and so much more. Each Intertech employee leads with the soft skills necessary to explain complex concepts to stakeholders and team members alike and makes your business more efficient, your data more valuable, and your team better. In addition, Intertech is a trusted partner of more than 4000 satisfied customers and has a 99.70% “would recommend” rating.