diff --git a/packages/libs/components/src/plots/PlotlyPlot.tsx b/packages/libs/components/src/plots/PlotlyPlot.tsx index a59d7b6a06..77f45427b2 100755 --- a/packages/libs/components/src/plots/PlotlyPlot.tsx +++ b/packages/libs/components/src/plots/PlotlyPlot.tsx @@ -18,6 +18,7 @@ import { PlotSpacingDefault, ColorPaletteAddon, ColorPaletteDefault, + VEuPathDBAnnotation, } from '../types/plots/addOns'; // add d3.select import { select } from 'd3'; @@ -68,6 +69,8 @@ export interface PlotProps extends ColorPaletteAddon { checkedLegendItems?: string[]; /** A function to call each time after plotly renders the plot */ onPlotlyRender?: PlotParams['onUpdate']; + /** array of annotations to show on the plot. Can be used with any plotly plot type */ + plotAnnotations?: VEuPathDBAnnotation[]; } const Plot = lazy(() => import('react-plotly.js')); @@ -111,6 +114,7 @@ function PlotlyPlot( checkedLegendItems, colorPalette = ColorPaletteDefault, onPlotlyRender, + plotAnnotations, ...plotlyProps } = props; @@ -141,6 +145,27 @@ function PlotlyPlot( const xAxisTitle = plotlyProps?.layout?.xaxis?.title; const yAxisTitle = plotlyProps?.layout?.yaxis?.title; + // Convert generalized annotation object to plotly-specific annotation + const plotlyAnnotations: PlotParams['layout']['annotations'] = useMemo(() => { + return plotAnnotations?.map((annotation) => { + return { + x: annotation.xSubject, + y: annotation.ySubject, + text: annotation.text, + xref: annotation.xref, + yref: annotation.yref, + xanchor: annotation.xAnchor, + yanchor: annotation.yAnchor, + ax: annotation.dx, + ay: annotation.dy, + showarrow: + typeof annotation.subjectConnector !== 'undefined' && + annotation.subjectConnector === 'arrow', + font: annotation.fontStyles, + }; + }); + }, [plotAnnotations]); + const finalLayout = useMemo( (): PlotParams['layout'] => ({ ...plotlyProps.layout, @@ -180,6 +205,7 @@ function PlotlyPlot( }, autosize: true, // responds properly to enclosing div resizing (not to be confused with config.responsive) colorway: colorPalette, + annotations: plotlyAnnotations, }), [ plotlyProps.layout, diff --git a/packages/libs/components/src/stories/plots/Histogram.stories.tsx b/packages/libs/components/src/stories/plots/Histogram.stories.tsx index f29daa0190..43d3554433 100644 --- a/packages/libs/components/src/stories/plots/Histogram.stories.tsx +++ b/packages/libs/components/src/stories/plots/Histogram.stories.tsx @@ -16,6 +16,7 @@ import { HistogramData, AxisTruncationConfig, FacetedData, + VEuPathDBAnnotation, } from '../../types/plots'; import FacetedHistogram from '../../plots/facetedPlots/FacetedHistogram'; @@ -560,3 +561,22 @@ Faceted.args = { }, }, }; + +const plotAnnotations: VEuPathDBAnnotation[] = [ + { + xref: 'paper', + yref: 'paper', + xSubject: 0.1, + xAnchor: 'left', + ySubject: 0.9, + yAnchor: 'top', + text: 'Annotation inside the plot', + }, +]; + +export const WithAnnotations = TemplateStaticWithRangeControls.bind({}); +WithAnnotations.args = { + data: staticData, + interactive: true, + plotAnnotations, +}; diff --git a/packages/libs/components/src/stories/plots/ScatterPlot.stories.tsx b/packages/libs/components/src/stories/plots/ScatterPlot.stories.tsx index f543ed4739..c8773af8b1 100755 --- a/packages/libs/components/src/stories/plots/ScatterPlot.stories.tsx +++ b/packages/libs/components/src/stories/plots/ScatterPlot.stories.tsx @@ -1,10 +1,12 @@ import React, { useState } from 'react'; import ScatterPlot, { ScatterPlotProps } from '../../plots/ScatterPlot'; import { Story, Meta } from '@storybook/react/types-6-0'; +import { PlotParams } from 'react-plotly.js'; // test to use RadioButtonGroup directly instead of ScatterPlotControls import RadioButtonGroup from '../../components/widgets/RadioButtonGroup'; import { FacetedData, ScatterPlotData } from '../../types/plots'; import FacetedScatterPlot from '../../plots/facetedPlots/FacetedScatterPlot'; +import { VEuPathDBAnnotation } from '../../types/plots'; import SliderWidget, { SliderWidgetProps, } from '../../components/widgets/Slider'; @@ -15,6 +17,7 @@ import { dateStringDataSet, processInputData, } from './ScatterPlot.storyData'; +import { Annotations } from 'plotly.js'; export default { title: 'Plots/ScatterPlot', @@ -383,3 +386,50 @@ export const opacitySlider = () => { ); }; + +// Plot annotations +const plotAnnotations: Array = [ + { + xSubject: 0.1, + ySubject: 0.9, + xref: 'x', + yref: 'y', + xAnchor: 'center', + yAnchor: 'top', + text: 'Annotation inside the plot, xy ref', + }, + { + xSubject: 1, + ySubject: 0, + xref: 'paper', + yref: 'paper', + xAnchor: 'left', + yAnchor: 'top', + dx: 5, + dy: 20, + text: 'Annotation outside the plot, paper ref', + }, + { + xSubject: 33, + ySubject: 3, + xref: 'x', + yref: 'y', + text: 'Annotating a point, fancy style', + subjectConnector: 'arrow', + dx: 0, + dy: -40, + fontStyles: { + family: 'Courier New, monospace', + size: 16, + color: 'blue', + }, + }, +]; + +export const PlotAnnotations: Story = Template.bind({}); +PlotAnnotations.args = { + data: dataSetProcess, + interactive: true, + displayLegend: true, + plotAnnotations, +}; diff --git a/packages/libs/components/src/types/plots/addOns.ts b/packages/libs/components/src/types/plots/addOns.ts index 86d5650fe9..4914f88eef 100644 --- a/packages/libs/components/src/types/plots/addOns.ts +++ b/packages/libs/components/src/types/plots/addOns.ts @@ -1,7 +1,7 @@ /** * Additional reusable modules to extend PlotProps and PlotData props */ -import { CSSProperties } from 'react'; +import { CSSProperties, ReactNode } from 'react'; import { BarLayoutOptions, OrientationOptions } from '.'; import { scaleLinear } from 'd3-scale'; import { interpolateLab, range } from 'd3'; @@ -358,3 +358,45 @@ export type AxisTruncationConfig = { max?: boolean; }; }; + +// Generalized annotation type for VEuPathDB plots +// Attemps to provide a uniform api for annotations, regardless of plotting library. +// For reference, plotly js annotation api: https://plotly.com/javascript/reference/layout/annotations/ +// visx annotation docs: https://airbnb.io/visx/docs/annotation +// Note, the 'subject' is the point on the plot for which we want to provide an annotation. This may be +// a data point, a part of the axis, etc. The 'subject' verbiage comes from visx. +export interface VEuPathDBAnnotation { + /** x position of the thing being annotated */ + xSubject: number; + /** y position of the thing being annotated */ + ySubject: number; + /** Text of the annotation */ + text: string; + /** Horizontal reference. + * Maps positions to the chart area (paper) or axis (x). + * See plotly "paper reference" for more. */ + xref: 'paper' | 'x'; + /** Vertical reference. + * Maps positions to the chart area (paper) or axis (y). + * See plotly "paper reference" for more. */ + yref: 'paper' | 'y'; + /** Horizontal alignment of the text */ + xAnchor?: 'left' | 'center' | 'right'; + /** Vertical alignment of the text */ + yAnchor?: 'top' | 'middle' | 'bottom'; + /** Horizontal text offset from the subject */ + dx?: number; + /** Vertical text offset from the subject */ + dy?: number; + /** Shape connecting the label to the subject */ + subjectConnector?: 'line' | 'arrow'; + /** Visx ONLY. Additional content to render wthin the visx Annotation component */ + children?: ReactNode; + /** Font styles */ + fontStyles?: { + size?: number; + color?: string; + family?: string; + weight?: number; + }; +}