Skip to content

Instantly share code, notes, and snippets.

@composite
Created March 5, 2025 05:21
Show Gist options
  • Save composite/b84b59fed5884c085376d152c45864d9 to your computer and use it in GitHub Desktop.
Save composite/b84b59fed5884c085376d152c45864d9 to your computer and use it in GitHub Desktop.
Plotly.js for React: Modern style instead of react-plotly.js, typescript and SSR friendly.
import { lazy, forwardRef, useEffect } from 'react';
export interface PlotlyProps extends HTMLAttributes<HTMLDivElement> {
data: Partial<Data>[];
layout?: Partial<Layout>;
config?: Partial<Config>;
}
export const Plotly = lazy(
() =>
import('plotly.js-dist-min').then((Plotly) => {
// Singleton ResizeObserver for all Plotly components
let resizeObserver: ResizeObserver | null = null;
const getResizeObserver = () => {
if (!resizeObserver) {
resizeObserver = new ResizeObserver((entries) => {
for (const entry of entries) {
const element = entry.target as HTMLDivElement;
if (element) {
// Using requestAnimationFrame to avoid too frequent updates
requestAnimationFrame(() => {
Plotly.Plots.resize(element);
});
}
}
});
}
return resizeObserver;
};
const PlotlyComponent = ({ data, layout, config, className, ...props }: PlotlyProps) => {
const plotRef = React.useRef<HTMLDivElement>(null);
const plotInstanceRef = React.useRef<PlotlyHTMLElement>(null);
useEffect(() => {
if (!plotRef.current) return;
const root = plotRef.current;
const defaultLayout: Partial<Layout> = {
autosize: true,
...layout,
};
const defaultConfig: Partial<Config> = {
responsive: true,
displayModeBar: true,
...config,
};
// Create new plot
Plotly.newPlot(plotRef.current, data, defaultLayout, defaultConfig).then((plot) => {
plotInstanceRef.current = plot;
});
// Cleanup
return () => Plotly.purge(root);
}, []); // Initial plot creation
// Update plot when data, layout, or config changes
useEffect(() => {
if (!plotRef.current || !plotInstanceRef.current) return;
const defaultLayout: Partial<Layout> = {
autosize: true,
...layout,
};
const defaultConfig: Partial<Config> = {
responsive: true,
displayModeBar: true,
...config,
};
void Plotly.react(plotRef.current, data, defaultLayout, defaultConfig);
}, [data, layout, config]);
// Handle resize using singleton ResizeObserver
useEffect(() => {
if (!plotRef.current) return;
const plot = plotRef.current;
const observer = getResizeObserver();
observer.observe(plot);
return () => observer.unobserve(plot);
}, []);
return (
<div ref={plotRef} className={className} {...props} />
);
};
return { default: PlotlyComponent };
})
);
import dynamic from 'next/dynamic';
import { forwardRef, useEffect } from 'react';
export interface PlotlyProps extends HTMLAttributes<HTMLDivElement> {
data: Partial<Data>[];
layout?: Partial<Layout>;
config?: Partial<Config>;
}
export const Plotly = dynamic(
() =>
import('plotly.js-dist-min').then((Plotly) => {
// Singleton ResizeObserver for all Plotly components
let resizeObserver: ResizeObserver | null = null;
const getResizeObserver = () => {
if (!resizeObserver) {
resizeObserver = new ResizeObserver((entries) => {
for (const entry of entries) {
const element = entry.target as HTMLDivElement;
if (element) {
// Using requestAnimationFrame to avoid too frequent updates
requestAnimationFrame(() => {
Plotly.Plots.resize(element);
});
}
}
});
}
return resizeObserver;
};
const PlotlyComponent = ({ data, layout, config, className, ...props }: PlotlyProps) => {
const plotRef = React.useRef<HTMLDivElement>(null);
const plotInstanceRef = React.useRef<PlotlyHTMLElement>(null);
useEffect(() => {
if (!plotRef.current) return;
const root = plotRef.current;
const defaultLayout: Partial<Layout> = {
autosize: true,
...layout,
};
const defaultConfig: Partial<Config> = {
responsive: true,
displayModeBar: true,
...config,
};
// Create new plot
Plotly.newPlot(plotRef.current, data, defaultLayout, defaultConfig).then((plot) => {
plotInstanceRef.current = plot;
});
// Cleanup
return () => Plotly.purge(root);
}, []); // Initial plot creation
// Update plot when data, layout, or config changes
useEffect(() => {
if (!plotRef.current || !plotInstanceRef.current) return;
const defaultLayout: Partial<Layout> = {
autosize: true,
...layout,
};
const defaultConfig: Partial<Config> = {
responsive: true,
displayModeBar: true,
...config,
};
void Plotly.react(plotRef.current, data, defaultLayout, defaultConfig);
}, [data, layout, config]);
// Handle resize using singleton ResizeObserver
useEffect(() => {
if (!plotRef.current) return;
const plot = plotRef.current;
const observer = getResizeObserver();
observer.observe(plot);
return () => observer.unobserve(plot);
}, []);
return (
<div ref={plotRef} className={className} {...props} />
);
};
return { default: PlotlyComponent };
}),
{ ssr: false }
);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment