From 9ad9012a6e869f696365aca14e0d35e755897c83 Mon Sep 17 00:00:00 2001 From: asizemore Date: Wed, 5 Feb 2025 10:41:16 -0500 Subject: [PATCH 1/5] added plot annotations functionality --- .../libs/components/src/plots/PlotlyPlot.tsx | 4 +++ .../src/stories/plots/ScatterPlot.stories.tsx | 33 +++++++++++++++++++ 2 files changed, 37 insertions(+) diff --git a/packages/libs/components/src/plots/PlotlyPlot.tsx b/packages/libs/components/src/plots/PlotlyPlot.tsx index a59d7b6a06..eed5bcc42d 100755 --- a/packages/libs/components/src/plots/PlotlyPlot.tsx +++ b/packages/libs/components/src/plots/PlotlyPlot.tsx @@ -68,6 +68,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. Paper referenced */ + plotAnnotations?: PlotParams['layout']['annotations']; } const Plot = lazy(() => import('react-plotly.js')); @@ -111,6 +113,7 @@ function PlotlyPlot( checkedLegendItems, colorPalette = ColorPaletteDefault, onPlotlyRender, + plotAnnotations, ...plotlyProps } = props; @@ -180,6 +183,7 @@ function PlotlyPlot( }, autosize: true, // responds properly to enclosing div resizing (not to be confused with config.responsive) colorway: colorPalette, + annotations: plotAnnotations, }), [ plotlyProps.layout, diff --git a/packages/libs/components/src/stories/plots/ScatterPlot.stories.tsx b/packages/libs/components/src/stories/plots/ScatterPlot.stories.tsx index f543ed4739..c71c541bd5 100755 --- a/packages/libs/components/src/stories/plots/ScatterPlot.stories.tsx +++ b/packages/libs/components/src/stories/plots/ScatterPlot.stories.tsx @@ -1,6 +1,7 @@ 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'; @@ -383,3 +384,35 @@ export const opacitySlider = () => { ); }; + +// Plot annotations +const plotAnnotations: PlotParams['layout']['annotations'] = [ + { + xref: 'paper', + yref: 'paper', + x: 0, + xanchor: 'right', + y: 1, + yanchor: 'bottom', + text: 'X axis label', + showarrow: false, + }, + { + xref: 'paper', + yref: 'paper', + x: 1, + xanchor: 'left', + y: 0, + yanchor: 'top', + text: 'Y axis label', + showarrow: false, + }, +]; + +export const Annotations: Story = Template.bind({}); +Annotations.args = { + data: dataSetProcess, + interactive: true, + displayLegend: true, + plotAnnotations: plotAnnotations, +}; From 0e0c3a9dd4ccf70f9c843177bcb6608c027c752c Mon Sep 17 00:00:00 2001 From: asizemore Date: Wed, 5 Feb 2025 10:48:37 -0500 Subject: [PATCH 2/5] improve scatter annotation story --- .../libs/components/src/plots/PlotlyPlot.tsx | 2 +- .../src/stories/plots/ScatterPlot.stories.tsx | 32 +++++++++++++++---- 2 files changed, 27 insertions(+), 7 deletions(-) diff --git a/packages/libs/components/src/plots/PlotlyPlot.tsx b/packages/libs/components/src/plots/PlotlyPlot.tsx index eed5bcc42d..37b6f12c48 100755 --- a/packages/libs/components/src/plots/PlotlyPlot.tsx +++ b/packages/libs/components/src/plots/PlotlyPlot.tsx @@ -68,7 +68,7 @@ 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. Paper referenced */ + /** array of annotations to show on the plot */ plotAnnotations?: PlotParams['layout']['annotations']; } diff --git a/packages/libs/components/src/stories/plots/ScatterPlot.stories.tsx b/packages/libs/components/src/stories/plots/ScatterPlot.stories.tsx index c71c541bd5..25d05aa422 100755 --- a/packages/libs/components/src/stories/plots/ScatterPlot.stories.tsx +++ b/packages/libs/components/src/stories/plots/ScatterPlot.stories.tsx @@ -390,11 +390,11 @@ const plotAnnotations: PlotParams['layout']['annotations'] = [ { xref: 'paper', yref: 'paper', - x: 0, - xanchor: 'right', - y: 1, - yanchor: 'bottom', - text: 'X axis label', + x: 0.1, + xanchor: 'left', + y: 0.9, + yanchor: 'top', + text: 'Annotation inside the plot', showarrow: false, }, { @@ -404,9 +404,29 @@ const plotAnnotations: PlotParams['layout']['annotations'] = [ xanchor: 'left', y: 0, yanchor: 'top', - text: 'Y axis label', + text: 'Annotation outside the plot', showarrow: false, }, + { + xref: 'x', + yref: 'y', + x: 33, + y: 3, + text: 'Annotating a point, fancy style', + showarrow: true, + ax: 0, + ay: -40, + font: { + family: 'Courier New, monospace', + size: 16, + color: '#ffffff', + }, + bordercolor: '#c7c7c7', + borderwidth: 2, + borderpad: 4, + bgcolor: '#ff20ff', + opacity: 0.8, + }, ]; export const Annotations: Story = Template.bind({}); From 15cf9b71dd0a0475f99d290a5130e7a4c5fb27cf Mon Sep 17 00:00:00 2001 From: asizemore Date: Wed, 5 Feb 2025 10:52:25 -0500 Subject: [PATCH 3/5] added histogram annotation story --- .../libs/components/src/plots/PlotlyPlot.tsx | 2 +- .../src/stories/plots/Histogram.stories.tsx | 20 +++++++++++++++++++ .../src/stories/plots/ScatterPlot.stories.tsx | 2 +- 3 files changed, 22 insertions(+), 2 deletions(-) diff --git a/packages/libs/components/src/plots/PlotlyPlot.tsx b/packages/libs/components/src/plots/PlotlyPlot.tsx index 37b6f12c48..162a0757c8 100755 --- a/packages/libs/components/src/plots/PlotlyPlot.tsx +++ b/packages/libs/components/src/plots/PlotlyPlot.tsx @@ -68,7 +68,7 @@ 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 */ + /** array of annotations to show on the plot. Can be used with any plotly plot type */ plotAnnotations?: PlotParams['layout']['annotations']; } diff --git a/packages/libs/components/src/stories/plots/Histogram.stories.tsx b/packages/libs/components/src/stories/plots/Histogram.stories.tsx index f29daa0190..4c85ab829b 100644 --- a/packages/libs/components/src/stories/plots/Histogram.stories.tsx +++ b/packages/libs/components/src/stories/plots/Histogram.stories.tsx @@ -18,6 +18,7 @@ import { FacetedData, } from '../../types/plots'; import FacetedHistogram from '../../plots/facetedPlots/FacetedHistogram'; +import { PlotParams } from 'react-plotly.js'; export default { title: 'Plots/Histogram', @@ -560,3 +561,22 @@ Faceted.args = { }, }, }; + +const plotAnnotations: PlotParams['layout']['annotations'] = [ + { + xref: 'paper', + yref: 'paper', + x: 0.1, + xanchor: 'left', + y: 0.9, + yanchor: 'top', + text: 'Annotation inside the plot', + showarrow: false, + }, +]; +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 25d05aa422..68a077df79 100755 --- a/packages/libs/components/src/stories/plots/ScatterPlot.stories.tsx +++ b/packages/libs/components/src/stories/plots/ScatterPlot.stories.tsx @@ -434,5 +434,5 @@ Annotations.args = { data: dataSetProcess, interactive: true, displayLegend: true, - plotAnnotations: plotAnnotations, + plotAnnotations, }; From 59b753de7c0b5d70378bb5755e2776bd73d2f2a8 Mon Sep 17 00:00:00 2001 From: asizemore Date: Mon, 10 Feb 2025 18:02:07 -0500 Subject: [PATCH 4/5] introduce VEuPathDBAnnotation type --- .../libs/components/src/plots/PlotlyPlot.tsx | 26 ++++++++- .../src/stories/plots/ScatterPlot.stories.tsx | 55 +++++++++---------- .../libs/components/src/types/plots/addOns.ts | 44 ++++++++++++++- 3 files changed, 93 insertions(+), 32 deletions(-) diff --git a/packages/libs/components/src/plots/PlotlyPlot.tsx b/packages/libs/components/src/plots/PlotlyPlot.tsx index 162a0757c8..a5c6ad3656 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'; @@ -69,7 +70,7 @@ export interface PlotProps extends ColorPaletteAddon { /** 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?: PlotParams['layout']['annotations']; + plotAnnotations?: VEuPathDBAnnotation[]; } const Plot = lazy(() => import('react-plotly.js')); @@ -144,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: + annotation.subjectConnector && + annotation.subjectConnector === 'arrow', + font: annotation.fontStyles, + }; + }); + }, [plotAnnotations]); + const finalLayout = useMemo( (): PlotParams['layout'] => ({ ...plotlyProps.layout, @@ -183,7 +205,7 @@ function PlotlyPlot( }, autosize: true, // responds properly to enclosing div resizing (not to be confused with config.responsive) colorway: colorPalette, - annotations: plotAnnotations, + annotations: plotlyAnnotations, }), [ plotlyProps.layout, diff --git a/packages/libs/components/src/stories/plots/ScatterPlot.stories.tsx b/packages/libs/components/src/stories/plots/ScatterPlot.stories.tsx index 68a077df79..c8773af8b1 100755 --- a/packages/libs/components/src/stories/plots/ScatterPlot.stories.tsx +++ b/packages/libs/components/src/stories/plots/ScatterPlot.stories.tsx @@ -6,6 +6,7 @@ import { PlotParams } from 'react-plotly.js'; 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'; @@ -16,6 +17,7 @@ import { dateStringDataSet, processInputData, } from './ScatterPlot.storyData'; +import { Annotations } from 'plotly.js'; export default { title: 'Plots/ScatterPlot', @@ -386,51 +388,46 @@ export const opacitySlider = () => { }; // Plot annotations -const plotAnnotations: PlotParams['layout']['annotations'] = [ +const plotAnnotations: Array = [ { - xref: 'paper', - yref: 'paper', - x: 0.1, - xanchor: 'left', - y: 0.9, - yanchor: 'top', - text: 'Annotation inside the plot', - showarrow: false, + 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', - x: 1, - xanchor: 'left', - y: 0, - yanchor: 'top', - text: 'Annotation outside the plot', - showarrow: false, + xAnchor: 'left', + yAnchor: 'top', + dx: 5, + dy: 20, + text: 'Annotation outside the plot, paper ref', }, { + xSubject: 33, + ySubject: 3, xref: 'x', yref: 'y', - x: 33, - y: 3, text: 'Annotating a point, fancy style', - showarrow: true, - ax: 0, - ay: -40, - font: { + subjectConnector: 'arrow', + dx: 0, + dy: -40, + fontStyles: { family: 'Courier New, monospace', size: 16, - color: '#ffffff', + color: 'blue', }, - bordercolor: '#c7c7c7', - borderwidth: 2, - borderpad: 4, - bgcolor: '#ff20ff', - opacity: 0.8, }, ]; -export const Annotations: Story = Template.bind({}); -Annotations.args = { +export const PlotAnnotations: Story = Template.bind({}); +PlotAnnotations.args = { data: dataSetProcess, interactive: true, displayLegend: true, 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; + }; +} From 6a411d2cdada74f78c00e6343ede0a8a332f4d2d Mon Sep 17 00:00:00 2001 From: asizemore Date: Tue, 11 Feb 2025 16:20:52 -0500 Subject: [PATCH 5/5] fix annotation types in histogram --- packages/libs/components/src/plots/PlotlyPlot.tsx | 2 +- .../src/stories/plots/Histogram.stories.tsx | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/libs/components/src/plots/PlotlyPlot.tsx b/packages/libs/components/src/plots/PlotlyPlot.tsx index a5c6ad3656..77f45427b2 100755 --- a/packages/libs/components/src/plots/PlotlyPlot.tsx +++ b/packages/libs/components/src/plots/PlotlyPlot.tsx @@ -159,7 +159,7 @@ function PlotlyPlot( ax: annotation.dx, ay: annotation.dy, showarrow: - annotation.subjectConnector && + typeof annotation.subjectConnector !== 'undefined' && annotation.subjectConnector === 'arrow', font: annotation.fontStyles, }; diff --git a/packages/libs/components/src/stories/plots/Histogram.stories.tsx b/packages/libs/components/src/stories/plots/Histogram.stories.tsx index 4c85ab829b..43d3554433 100644 --- a/packages/libs/components/src/stories/plots/Histogram.stories.tsx +++ b/packages/libs/components/src/stories/plots/Histogram.stories.tsx @@ -16,9 +16,9 @@ import { HistogramData, AxisTruncationConfig, FacetedData, + VEuPathDBAnnotation, } from '../../types/plots'; import FacetedHistogram from '../../plots/facetedPlots/FacetedHistogram'; -import { PlotParams } from 'react-plotly.js'; export default { title: 'Plots/Histogram', @@ -562,18 +562,18 @@ Faceted.args = { }, }; -const plotAnnotations: PlotParams['layout']['annotations'] = [ +const plotAnnotations: VEuPathDBAnnotation[] = [ { xref: 'paper', yref: 'paper', - x: 0.1, - xanchor: 'left', - y: 0.9, - yanchor: 'top', + xSubject: 0.1, + xAnchor: 'left', + ySubject: 0.9, + yAnchor: 'top', text: 'Annotation inside the plot', - showarrow: false, }, ]; + export const WithAnnotations = TemplateStaticWithRangeControls.bind({}); WithAnnotations.args = { data: staticData,