Part 2 of 3: Controlling Plotly in React – Fully Custom Annotations
Displaying custom annotations on scatterplot click
But doesn’t plotly js already allow custom annotations?
Yes, but they’re limited to only a small set of basic HTML elements. This tutorial will show you how to create your own annotations that consist of any JSX element.
You should already have a basic understanding of:
- Javascript
- Typescript
- Plotly in react
- React and React hooks
Steps in creating these custom annotations in plotly
- Create starter app from Part 1 of this series
- Install additional dependencies
- Create AnnotationContent
- Create AnnotationCircle
- Create custom hook with initial state
- Add Tooltip with Styling
- Add hook and component into main app
Create starter app from Part 1 of this series
Go to part 1 of this 3 part series and follow the following steps:
- Create the app in codesandbox.io
- Import dependencies and starting code
Install additional dependencies
For this new application we’ll be using MUI and a very nifty library called “react-use”
Install the following additional dependencies:
- @emotion/react
- @emotion/styled
- @mui/material
- React-use
Your dependency list should then look like this:
Create AnnotationContent
We want our annotation to contain the following items:
- Some text
- Data from the point that we clicked on
- An image
- A button to press.
Steps
- Right click on “src” and select Create File. Name the new file “AnnotationContent.tsx”
- Paste in the following code:
import { Button, Paper, Stack, Typography } from "@mui/material";
import React from "react";
interface AnnotationContentProps {
text: string;
onCloseClick(): void;
}
export const AnnotationContent = ({
text,
onCloseClick
}: AnnotationContentProps) => (
<Paper elevation={0}>
<Stack>
<Typography>A custom annotation!</Typography>
<Typography>{text}</Typography>
<div>
<img alt="" src="https://placekitten.com/75/75" />
</div>
<Button onClick={onCloseClick}>Close</Button>
</Stack>
</Paper>
);
Create AnnotationCircle
We want to create a circle that will go around the point that we’ve clicked. This circle will be a simple element that anchors the tooltip.
Steps
- Right click on the “src” directory and select Create File. Name the new file “AnnotationCircle.tsx”.
- Paste in the following code:
import { Box } from "@mui/material";
import React from "react";
interface AnnotationCircleProps {
top: number;
left: number;
diameter: number;
}
export const AnnotationCircle = React.forwardRef(
({ top, left, diameter, ...rest }: AnnotationCircleProps, ref) => (
<Box
ref={ref}
style={{
position: "absolute",
top: `calc(${top}px - ${diameter / 2}px)`,
left: `calc(${left}px - ${diameter / 2}px)`,
display: "inline-block",
backgroundColor: "transparent",
border: `2px solid black`,
borderRadius: "50%",
content: '""',
width: diameter,
height: diameter
}}
{...rest}
/>
)
);
Explanation of code
- Because we’re creating a custom component that will be the anchor for a tooltip, we need to pass a reference back up to the soon-to-be parent tooltip. You can read more about this pattern here: https://mui.com/material-ui/react-tooltip/#custom-child-element
- This element needs to be a transparent circle that is centered on the point that is clicked.
Create custom hook with initial state
Next we’ll create the custom hook that uses AnnotationCircle and AnnotationContent within a MUI Tooltip.
Steps
- Right click on the “src” directory and select Create File. Name this new file “useCustomAnnotation.ts”
- Add the following imports to the top of the file:
import { styled, Tooltip, tooltipClasses, TooltipProps } from "@mui/material";
import { PlotMouseEvent } from "plotly.js";
import { useEffect, useState } from "react";
import { useWindowSize } from "react-use";
import { AnnotationCircle } from "./AnnotationCircle";
import { AnnotationContent } from "./AnnotationContent";
- Paste the following code underneath:
export const useCustomAnnotation = () => {
const { width, height } = useWindowSize();
const [annotationData, setAnnotationData] = useState<
PlotMouseEvent | undefined
>();
// close the annotation if the window resizes
useEffect(() => {
setAnnotationData(undefined);
}, [width, height]);
}
Explanation of Code
- We’re adding all the imports we’ll eventually be using, so some will show as “unused” until the next step.
- The “annotationData” variable is what will contain our mouse event, which will hold the (x,y) coordinates of point we click on.
- We need to keep track of the window height and size because we need to close the annotation if the user changes the window size. This is because the coordinates of the point would change if the screen size changed but the annotation would stay fixed at the previous, no outdated, coordinates. So for ease we’ll simply close the clear the annotation.
Add Tooltip with Styling
Finally we’ll add the actual MUI Tooltip to the hook and link the AnnotationCircle and AnnotationContent inside of it. We will apply some styling to the Tooltip so it matches the content Paper element.
Steps
- In App.tsx after the imports but before the hook, paste the following code:
const circleDiameter = 20;
const StyledTooltip = styled(({ className, ...props }: TooltipProps) => (
<Tooltip {...props} classes={{ popper: className }} />
))(() => ({
[`& .${tooltipClasses.tooltip}`]: {
backgroundColor: "white",
border: `1px solid black`
},
[`& .${tooltipClasses.arrow}`]: {
color: "black"
}
}));
- Inside the hook after the useEffect you wrote in the previous step, paste the following code:
const coordString = `(${annotationData?.points[0].x},${annotationData?.points[0].y})`;
const CustomAnnotation = () => {
if (!annotationData) return <></>;
return (
<StyledTooltip
open
arrow
PopperProps={{
disablePortal: true,
placement: "auto",
modifiers: [
{
name: "offset",
options: {
offset: [0, -10]
}
}
]
}}
title={
<AnnotationContent
text={coordString}
onCloseClick={() => setAnnotationData(undefined)}
/>
}
>
<AnnotationCircle
left={annotationData.event.x}
top={annotationData.event.y}
diameter={circleDiameter}
/>
</StyledTooltip>
);
};
return { CustomAnnotation, setAnnotationData };
Explanation of Code
- The annotation should only return the tooltip if annotation data exists
- MUI’s tooltip is built on top of popper.js, so we modify the PopperProps to ensure the tooltip is close to the AnnotationCircle and that the annotation is automatically placed in a spot where it won’t render off the screen
- annotationData contains the mouse event of where the user clicked and we pass that information to AnnotationCircle so it knows where to render the circle.
Add hook and component into main app
Now that we’ve made our custom hook we can add it into our main app component!
Steps
- Paste the following import into App.tsx
import { useCustomAnnotation } from "./useCustomAnnotation";
- Inside your app component, use the hook and extract our the return elements:
const { CustomAnnotation, setAnnotationData } = useCustomAnnotation();
- In your Plot component, set the onClick prop to call setAnnotationData
onClick={setAnnotationData}
- Finally, add the CustomAnnotation component next to the Plot element
<CustomAnnotation />
Conclusion
And that’s how you add a “custom annotation” to a Plotly app!
Here is a finished sandbox: https://codesandbox.io/s/plotly-custom-annotations-0dexmi?file=/src/App.tsx
In the next article, I’ll be showing you how to give each marker a custom color based on its category.
Parts
Part 1 of 3: Controlling Plotly in React – Control the Mode Bar
Part 2 of 3: Controlling Plotly in React – Fully Custom Annotations
Part 3 of 3: Controlling Plotly in React – Adding Custom Color Markers by Category
About Black Slate
Black Slate 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. Black Slate 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 Black Slate 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, Black Slate is a trusted partner of more than 4000 satisfied customers and has a 99.70% “would recommend” rating.