From 87d9a50dc07d89715c330ed84bebfc3d2762ea91 Mon Sep 17 00:00:00 2001 From: Teodor Voicu Date: Wed, 20 May 2026 00:27:28 +0300 Subject: [PATCH 1/3] Print styles --- src/less/plotly.less | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/src/less/plotly.less b/src/less/plotly.less index e883004..d698731 100644 --- a/src/less/plotly.less +++ b/src/less/plotly.less @@ -109,3 +109,32 @@ } } } + +@media print { + .visualization:not(.autoscale) { + .plot-container { + overflow: visible !important; + } + } + + .plotly-component, + .visualization, + .plot-container, + .svg-container, + .js-plotly-plot { + max-width: 100% !important; + break-inside: avoid; + page-break-inside: avoid; + } + + .svg-container, + .main-svg { + overflow: visible !important; + } + + .modebar, + .modebar-container, + .visualization-toolbar { + display: none !important; + } +} From 1b115eb1f3f10b5eaf9d741fa6a3def5bd2b0d92 Mon Sep 17 00:00:00 2001 From: Teodor Voicu Date: Mon, 25 May 2026 21:41:24 +0300 Subject: [PATCH 2/3] add tests to increase coverage --- jest-addon.config.js | 15 +-- src/Blocks/EmbedVisualization/Edit.test.jsx | 80 ++++++++++++++ src/Blocks/EmbedVisualization/View.test.jsx | 36 +++++++ src/Blocks/PlotlyChart/Edit.test.jsx | 104 +++++++++++++++++++ src/Blocks/PlotlyChart/View.test.jsx | 28 +++++ src/Blocks/Treemap/Edit.test.jsx | 79 ++++++++++++++ src/Blocks/Treemap/View.test.jsx | 23 ++++ src/PlotlyComponent/Placeholder.test.jsx | 14 +++ src/Views/VisualizationView.test.jsx | 60 +++++++++++ src/Widgets/VisualizationViewWidget.test.jsx | 62 +++++++++++ src/actions.test.js | 22 ++++ src/reducers/data_visualizations.test.js | 62 +++++++++++ 12 files changed, 579 insertions(+), 6 deletions(-) create mode 100644 src/Blocks/EmbedVisualization/Edit.test.jsx create mode 100644 src/Blocks/EmbedVisualization/View.test.jsx create mode 100644 src/Blocks/PlotlyChart/Edit.test.jsx create mode 100644 src/Blocks/PlotlyChart/View.test.jsx create mode 100644 src/Blocks/Treemap/Edit.test.jsx create mode 100644 src/Blocks/Treemap/View.test.jsx create mode 100644 src/PlotlyComponent/Placeholder.test.jsx create mode 100644 src/Views/VisualizationView.test.jsx create mode 100644 src/Widgets/VisualizationViewWidget.test.jsx create mode 100644 src/actions.test.js create mode 100644 src/reducers/data_visualizations.test.js diff --git a/jest-addon.config.js b/jest-addon.config.js index aef0f2b..84dd00e 100644 --- a/jest-addon.config.js +++ b/jest-addon.config.js @@ -1,19 +1,22 @@ -require('dotenv').config({ path: __dirname + '/.env' }) +require('dotenv').config({ path: __dirname + '/.env' }); -const fs = require('fs') -const path = require('path') +const fs = require('fs'); +const path = require('path'); const voltoSlatePath = fs.existsSync( path.join(__dirname, '../../../node_modules/@plone/volto-slate/src'), ) ? '/node_modules/@plone/volto-slate/src' - : '/node_modules/@plone/volto/packages/volto-slate/src' + : '/node_modules/@plone/volto/packages/volto-slate/src'; module.exports = { testMatch: ['**/src/addons/**/?(*.)+(spec|test).[jt]s?(x)'], collectCoverageFrom: [ - 'src/addons/**/src/**/*.{js,jsx,ts,tsx}', + 'src/addons/volto-plotlycharts/src/**/*.{js,jsx,ts,tsx}', '!src/**/*.d.ts', + '!**/*.test.{js,jsx,ts,tsx}', + '!**/*.stories.{js,jsx,ts,tsx}', + '!**/*.spec.{js,jsx,ts,tsx}', ], moduleNameMapper: { '\\.(css|less|scss|sass)$': 'identity-obj-proxy', @@ -55,4 +58,4 @@ module.exports = { '/node_modules/@eeacms/volto-plotlycharts/jest.setup.js', ], }), -} +}; diff --git a/src/Blocks/EmbedVisualization/Edit.test.jsx b/src/Blocks/EmbedVisualization/Edit.test.jsx new file mode 100644 index 0000000..c34b229 --- /dev/null +++ b/src/Blocks/EmbedVisualization/Edit.test.jsx @@ -0,0 +1,80 @@ +import React from 'react'; +import renderer, { act } from 'react-test-renderer'; +import Edit from './Edit'; + +jest.mock('semantic-ui-react', () => ({ + Message: ({ children }) =>
{children}
, +})); + +jest.mock('@plone/volto/components/manage/Sidebar/SidebarPortal', () => ({ + __esModule: true, + default: ({ children, selected }) => ( + + ), +})); + +jest.mock('@plone/volto/components/manage/Form/BlockDataForm', () => ({ + __esModule: true, + default: (props) => ( +
+ ), +})); + +jest.mock('@eeacms/volto-plotlycharts/PlotlyComponent', () => (props) => ( +
+)); + +describe('EmbedVisualization Edit', () => { + it('shows a message until a visualization is selected', () => { + const component = renderer.create( + , + ); + + expect( + component.root.findByProps({ className: 'mock-message' }), + ).toBeTruthy(); + expect( + component.root.findByProps({ className: 'mock-block-data-form' }), + ).toBeTruthy(); + }); + + it('renders PlotlyComponent and updates block data from the sidebar form', () => { + const onChangeBlock = jest.fn(); + const component = renderer.create( + , + ); + const plotly = component.root.findByProps({ + className: 'mock-plotly-component', + }); + const form = component.root.findByProps({ + className: 'mock-block-data-form', + }); + + expect(plotly.props['data-props'].mode).toBe('edit'); + expect(plotly.props['data-props'].data).toMatchObject({ + vis_url: '/visualization', + download_button: true, + has_data_query_by_context: true, + with_sources: true, + with_more_info: true, + with_notes: false, + }); + + act(() => { + form.props['data-props'].onChangeField('title', 'Updated'); + }); + + expect(onChangeBlock).toHaveBeenCalledWith('block-id', { + vis_url: '/visualization', + with_notes: false, + title: 'Updated', + }); + }); +}); diff --git a/src/Blocks/EmbedVisualization/View.test.jsx b/src/Blocks/EmbedVisualization/View.test.jsx new file mode 100644 index 0000000..a14b3aa --- /dev/null +++ b/src/Blocks/EmbedVisualization/View.test.jsx @@ -0,0 +1,36 @@ +import React from 'react'; +import renderer from 'react-test-renderer'; +import View from './View'; + +jest.mock('@eeacms/volto-plotlycharts/PlotlyComponent', () => (props) => ( +
+)); + +describe('EmbedVisualization View', () => { + it('passes embedded visualization defaults to PlotlyComponent', () => { + const component = renderer.create( + , + ); + const plotly = component.root.findByProps({ + className: 'mock-plotly-component', + }); + + expect( + component.root.findByProps({ className: 'embed-visualization view' }), + ).toBeTruthy(); + expect(plotly.props['data-props']).toMatchObject({ mode: 'preview' }); + expect(plotly.props['data-props'].data).toMatchObject({ + title: 'Embedded chart', + download_button: true, + has_data_query_by_context: true, + with_sources: true, + llm_summary: 'Summary', + }); + }); +}); diff --git a/src/Blocks/PlotlyChart/Edit.test.jsx b/src/Blocks/PlotlyChart/Edit.test.jsx new file mode 100644 index 0000000..1c7c19b --- /dev/null +++ b/src/Blocks/PlotlyChart/Edit.test.jsx @@ -0,0 +1,104 @@ +import React from 'react'; +import renderer, { act } from 'react-test-renderer'; +import Edit from './Edit'; + +jest.mock('semantic-ui-react', () => { + const Modal = ({ children, open, className }) => ( +
+ {children} +
+ ); + Modal.Content = ({ children, scrolling }) => ( +
+ {children} +
+ ); + + return { + Button: ({ children, onClick }) => ( + + ), + Modal, + }; +}); + +jest.mock('@plone/volto/components/manage/Sidebar/SidebarPortal', () => ({ + __esModule: true, + default: ({ children }) => , +})); + +jest.mock('@plone/volto/components/manage/Form/BlockDataForm', () => ({ + __esModule: true, + default: (props) => ( + + ), +})); + +jest.mock('./View', () => (props) => ( +
+)); + +describe('PlotlyChart Edit', () => { + beforeEach(() => { + global.__SERVER__ = false; + }); + + it('renders editor controls, block view, and sidebar form', () => { + const component = renderer.create( + , + ); + + expect(component.root.findByType('button').children).toEqual([ + 'Open Chart Editor', + ]); + expect( + component.root.findByProps({ className: 'mock-plotly-view' }).props[ + 'data-props' + ].mode, + ).toBe('edit'); + expect( + component.root.findByProps({ className: 'mock-block-data-form' }), + ).toBeTruthy(); + }); + + it('opens the chart editor modal and updates fields from the sidebar form', () => { + const onChangeBlock = jest.fn(); + const preventDefault = jest.fn(); + const stopPropagation = jest.fn(); + const component = renderer.create( + , + ); + + act(() => { + component.root.findByType('button').props.onClick({ + preventDefault, + stopPropagation, + }); + }); + + expect(preventDefault).toHaveBeenCalled(); + expect(stopPropagation).toHaveBeenCalled(); + expect( + component.root.findByProps({ + className: 'chart-editor-modal plotly-editor--theme-provider', + }), + ).toBeTruthy(); + + act(() => { + component.root + .findByProps({ className: 'mock-block-data-form' }) + .props['data-props'].onChangeField('title', 'Updated'); + }); + + expect(onChangeBlock).toHaveBeenCalledWith('block-id', { + title: 'Updated', + visualization: { data: [] }, + }); + }); +}); diff --git a/src/Blocks/PlotlyChart/View.test.jsx b/src/Blocks/PlotlyChart/View.test.jsx new file mode 100644 index 0000000..0f57dce --- /dev/null +++ b/src/Blocks/PlotlyChart/View.test.jsx @@ -0,0 +1,28 @@ +import React from 'react'; +import renderer from 'react-test-renderer'; +import View from './View'; + +jest.mock('@eeacms/volto-plotlycharts/PlotlyComponent', () => (props) => ( +
+)); + +describe('PlotlyChart View', () => { + it('renders PlotlyComponent with default print/export options', () => { + const component = renderer.create( + , + ); + const plotly = component.root.findByProps({ + className: 'mock-plotly-component', + }); + + expect( + component.root.findByProps({ className: 'plotly-chart' }), + ).toBeTruthy(); + expect(plotly.props['data-props'].data).toMatchObject({ + title: 'Chart', + download_button: true, + has_data_query_by_context: true, + with_sources: false, + }); + }); +}); diff --git a/src/Blocks/Treemap/Edit.test.jsx b/src/Blocks/Treemap/Edit.test.jsx new file mode 100644 index 0000000..8c9aafc --- /dev/null +++ b/src/Blocks/Treemap/Edit.test.jsx @@ -0,0 +1,79 @@ +import React from 'react'; +import renderer, { act } from 'react-test-renderer'; +import Edit from './Edit'; + +jest.mock('redux', () => ({ + compose: + (...fns) => + (value) => + fns.reduceRight((acc, fn) => fn(acc), value), +})); + +jest.mock('@eeacms/volto-datablocks/hocs', () => ({ + connectToProviderData: () => (Component) => (props) => ( + + ), +})); + +jest.mock('@plone/volto/components/manage/Sidebar/SidebarPortal', () => ({ + __esModule: true, + default: ({ children }) => , +})); + +jest.mock('@plone/volto/components/manage/Form/BlockDataForm', () => ({ + __esModule: true, + default: (props) => ( + + ), +})); + +jest.mock('./View', () => (props) => ( +
+)); + +describe('Treemap Edit', () => { + it('renders TreemapView and builds schema choices from provider data', () => { + const component = renderer.create( + , + ); + const form = component.root.findByProps({ + className: 'mock-block-data-form', + }); + + expect( + component.root.findByProps({ className: 'mock-treemap-view' }), + ).toBeTruthy(); + expect( + form.props['data-props'].schema.properties.size_column.choices, + ).toEqual([ + ['size', 'size'], + ['label', 'label'], + ]); + }); + + it('updates block data from the sidebar form', () => { + const onChangeBlock = jest.fn(); + const component = renderer.create( + , + ); + const form = component.root.findByProps({ + className: 'mock-block-data-form', + }); + + act(() => { + form.props['data-props'].onChangeField('size_column', 'size'); + }); + + expect(onChangeBlock).toHaveBeenCalledWith('block-id', { + url: '/data', + size_column: 'size', + }); + }); +}); diff --git a/src/Blocks/Treemap/View.test.jsx b/src/Blocks/Treemap/View.test.jsx new file mode 100644 index 0000000..1feb70a --- /dev/null +++ b/src/Blocks/Treemap/View.test.jsx @@ -0,0 +1,23 @@ +import React from 'react'; +import renderer from 'react-test-renderer'; +import View from './View'; + +jest.mock('./Treemap', () => (props) => ( +
+)); + +jest.mock('@eeacms/volto-datablocks/hocs', () => ({ + withBlockData: (Component) => (props) => ( + + ), +})); + +describe('Treemap View', () => { + it('renders Treemap with block data props', () => { + const component = renderer.create(); + const treemap = component.root.findByProps({ className: 'mock-treemap' }); + + expect(treemap.props['data-props'].data).toEqual({ title: 'Treemap' }); + expect(treemap.props['data-props'].provider_data).toEqual({ value: [1] }); + }); +}); diff --git a/src/PlotlyComponent/Placeholder.test.jsx b/src/PlotlyComponent/Placeholder.test.jsx new file mode 100644 index 0000000..e2d3332 --- /dev/null +++ b/src/PlotlyComponent/Placeholder.test.jsx @@ -0,0 +1,14 @@ +import React from 'react'; +import renderer from 'react-test-renderer'; +import Placeholder from './Placeholder'; + +describe('Placeholder', () => { + it('renders the Plotly loading placeholder SVG', () => { + const component = renderer.create(); + const root = component.root; + + expect(root.findByProps({ className: 'plotly-placeholder' })).toBeTruthy(); + expect(root.findByType('svg').props.viewBox).toBe('0 0 240 240'); + expect(root.findAllByProps({ className: 'bar' })).toHaveLength(5); + }); +}); diff --git a/src/Views/VisualizationView.test.jsx b/src/Views/VisualizationView.test.jsx new file mode 100644 index 0000000..b76dab6 --- /dev/null +++ b/src/Views/VisualizationView.test.jsx @@ -0,0 +1,60 @@ +import React from 'react'; +import renderer from 'react-test-renderer'; +import VisualizationView from './VisualizationView'; + +jest.mock('semantic-ui-react', () => ({ + Container: ({ children, ...props }) =>
{children}
, +})); + +jest.mock('@plone/volto/helpers/Blocks/Blocks', () => ({ + hasBlocksData: jest.fn((content) => Boolean(content.blocks_layout)), +})); + +jest.mock('@plone/volto/components/theme/View/RenderBlocks', () => (props) => ( +
+)); + +jest.mock('@eeacms/volto-embed/helpers', () => ({ + pickMetadata: jest.fn((content) => ({ picked: content.title })), +})); + +jest.mock('@eeacms/volto-plotlycharts/PlotlyComponent', () => (props) => ( +
+)); + +describe('VisualizationView', () => { + it('renders PlotlyComponent when content has no blocks', () => { + const content = { + title: 'Visualization', + visualization: { '@id': '/chart' }, + llm_summary: 'Summary', + }; + const component = renderer.create(); + const plotly = component.root.findByProps({ + className: 'mock-plotly-component', + }); + + expect(component.root.findByProps({ id: 'page-document' })).toBeTruthy(); + expect(plotly.props['data-props'].data).toMatchObject({ + with_sources: false, + with_notes: false, + with_more_info: false, + download_button: true, + with_enlarge: true, + with_share: true, + properties: { picked: 'Visualization' }, + visualization: { '@id': '/chart' }, + llm_summary: 'Summary', + }); + }); + + it('renders blocks when content has blocks data', () => { + const component = renderer.create( + , + ); + + expect( + component.root.findByProps({ className: 'mock-render-blocks' }), + ).toBeTruthy(); + }); +}); diff --git a/src/Widgets/VisualizationViewWidget.test.jsx b/src/Widgets/VisualizationViewWidget.test.jsx new file mode 100644 index 0000000..ec799dd --- /dev/null +++ b/src/Widgets/VisualizationViewWidget.test.jsx @@ -0,0 +1,62 @@ +import React from 'react'; +import renderer from 'react-test-renderer'; +import VisualizationViewWidget from './VisualizationViewWidget'; + +jest.mock('react-redux', () => ({ + connect: (mapStateToProps) => (Component) => (props) => ( + + ), +})); + +jest.mock('@eeacms/volto-embed/helpers', () => ({ + pickMetadata: jest.fn((content) => ({ picked: content.title })), +})); + +jest.mock('@eeacms/volto-plotlycharts/PlotlyComponent', () => (props) => ( +
+)); + +describe('VisualizationViewWidget', () => { + it('uses provided content metadata and selected visualization value', () => { + const component = renderer.create( + , + ); + const plotly = component.root.findByProps({ + className: 'mock-plotly-component', + }); + + expect(plotly.props['data-props'].data).toMatchObject({ + with_sources: false, + with_notes: false, + with_more_info: false, + download_button: true, + with_enlarge: true, + with_share: true, + visualization: { '@id': '/visualization' }, + properties: { picked: 'Provided title' }, + llm_summary: 'Provided summary', + }); + }); + + it('falls back to content from state when content prop is missing', () => { + const component = renderer.create( + , + ); + const plotly = component.root.findByProps({ + className: 'mock-plotly-component', + }); + + expect(plotly.props['data-props'].data.properties).toEqual({ + picked: 'State title', + }); + }); +}); diff --git a/src/actions.test.js b/src/actions.test.js new file mode 100644 index 0000000..484236e --- /dev/null +++ b/src/actions.test.js @@ -0,0 +1,22 @@ +import { getVisualization, removeVisualization } from './actions'; +import { GET_VISUALIZATION, REMOVE_VISUALIZATION } from './constants'; + +describe('plotlycharts actions', () => { + it('creates a visualization request action', () => { + expect(getVisualization('/chart')).toEqual({ + type: GET_VISUALIZATION, + path: '/chart', + request: { + op: 'get', + path: '/chart/@visualization', + }, + }); + }); + + it('creates a remove visualization action', () => { + expect(removeVisualization('/chart')).toEqual({ + type: REMOVE_VISUALIZATION, + path: '/chart', + }); + }); +}); diff --git a/src/reducers/data_visualizations.test.js b/src/reducers/data_visualizations.test.js new file mode 100644 index 0000000..94cb4d2 --- /dev/null +++ b/src/reducers/data_visualizations.test.js @@ -0,0 +1,62 @@ +import reducer from './data_visualizations'; +import { GET_VISUALIZATION, REMOVE_VISUALIZATION } from '../constants'; + +describe('data_visualizations reducer', () => { + it('tracks pending visualization requests', () => { + const state = reducer(undefined, { + type: `${GET_VISUALIZATION}_PENDING`, + path: '/chart/@visualization', + }); + + expect(state.loading).toBe(true); + expect(state.pendingVisualizations).toEqual({ '/chart': true }); + expect(state.requested).toEqual(['/chart']); + }); + + it('stores loaded visualizations and removes pending state', () => { + const state = reducer( + { + data: {}, + pendingVisualizations: { '/chart': true }, + failedVisualizations: {}, + requested: ['/chart'], + }, + { + type: `${GET_VISUALIZATION}_SUCCESS`, + path: '/chart/@visualization', + result: { title: 'Chart' }, + }, + ); + + expect(state.loaded).toBe(true); + expect(state.loading).toBe(false); + expect(state.data).toEqual({ '/chart': { title: 'Chart' } }); + expect(state.pendingVisualizations).toEqual({}); + }); + + it('marks failed requests and removes stored visualizations', () => { + const failed = reducer( + { + data: { '/chart': { title: 'Old chart' } }, + pendingVisualizations: { '/chart': true }, + failedVisualizations: {}, + requested: ['/chart'], + }, + { + type: `${GET_VISUALIZATION}_FAIL`, + path: '/chart/@visualization', + error: 'Failed', + }, + ); + + expect(failed.error).toBe('Failed'); + expect(failed.failedVisualizations).toEqual({ '/chart': true }); + + const removed = reducer(failed, { + type: REMOVE_VISUALIZATION, + path: '/chart', + }); + + expect(removed.data).toEqual({}); + }); +}); From 861bc81e6ca7621a4af0891ea957342b5cffd9a1 Mon Sep 17 00:00:00 2001 From: Teodor Voicu Date: Tue, 26 May 2026 16:16:50 +0300 Subject: [PATCH 3/3] fix display of plotlycharts --- src/PlotlyComponent/Plot.jsx | 132 +++++++++++++++++++++++++++++++++-- src/less/plotly.less | 29 -------- 2 files changed, 128 insertions(+), 33 deletions(-) diff --git a/src/PlotlyComponent/Plot.jsx b/src/PlotlyComponent/Plot.jsx index 060cd87..14ae730 100644 --- a/src/PlotlyComponent/Plot.jsx +++ b/src/PlotlyComponent/Plot.jsx @@ -1,4 +1,4 @@ -import React, { forwardRef, useMemo } from 'react'; +import React, { forwardRef, useEffect, useMemo, useRef } from 'react'; import { injectLazyLibs } from '@plone/volto/helpers/Loadable/Loadable'; import { useHistory } from 'react-router-dom'; @@ -16,6 +16,8 @@ const Plot = forwardRef((props, ref) => { const plotly = props.plotlyMinLib?.default || props.plotlyMinLib; const plotlyComponentFactory = props.plotlyComponentFactory?.default || props.plotlyComponentFactory; + const graphDiv = useRef(null); + const originalPrintLayout = useRef(null); const PlotlyComponent = useMemo(() => { return plotlyComponentFactory(plotly); @@ -94,6 +96,130 @@ const Plot = forwardRef((props, ref) => { } }; + const setGraphDiv = (value) => { + graphDiv.current = value; + + if (typeof ref === 'function') { + ref(value); + } else if (ref) { + ref.current = value; + } + }; + + useEffect(() => { + if (typeof window === 'undefined') return undefined; + + const resizePlot = () => { + if (!graphDiv.current || !plotly?.Plots?.resize) return; + + try { + plotly.Plots.resize(graphDiv.current); + } catch { + // Plotly can throw if the graph is being unmounted during print cleanup. + } + }; + + const relayoutPlot = (layoutUpdate) => { + if (!graphDiv.current || !plotly?.relayout) return false; + + try { + const result = plotly.relayout(graphDiv.current, layoutUpdate); + result?.catch?.(() => {}); + return true; + } catch { + return false; + } + }; + + const getWidth = (element) => + Math.floor(element?.getBoundingClientRect?.().width || 0); + + const getPrintWidth = () => { + const chart = graphDiv.current?.closest?.( + '.embed-visualization, .plotly-component', + ); + const content = graphDiv.current?.closest?.( + '#page-document, .content-area', + ); + const widths = [ + getWidth(chart), + getWidth(content), + getWidth(graphDiv.current?.parentElement), + ].filter((width) => width > 0); + + return widths.length ? Math.min(...widths) : 0; + }; + + const fitPlotToPrintWidth = () => { + const targetWidth = getPrintWidth(); + const currentWidth = + graphDiv.current?._fullLayout?.width || layout?.width || 0; + + if (!targetWidth || !currentWidth || currentWidth <= targetWidth) { + resizePlot(); + return; + } + + if (!originalPrintLayout.current) { + originalPrintLayout.current = { + width: graphDiv.current?._fullLayout?.width || layout?.width, + height: graphDiv.current?._fullLayout?.height || layout?.height, + }; + } + + if (!relayoutPlot({ width: targetWidth })) { + resizePlot(); + } + }; + + const restorePlotWidth = () => { + if (!originalPrintLayout.current) { + resizePlot(); + return; + } + + const { width, height } = originalPrintLayout.current; + originalPrintLayout.current = null; + + const layoutUpdate = { + ...(width ? { width } : {}), + ...(height ? { height } : {}), + }; + + if (!Object.keys(layoutUpdate).length || !relayoutPlot(layoutUpdate)) { + resizePlot(); + } + }; + + const handleBeforePrint = () => { + fitPlotToPrintWidth(); + const nextFrame = window.requestAnimationFrame + ? (callback) => window.requestAnimationFrame(callback) + : (callback) => window.setTimeout(callback, 0); + nextFrame(fitPlotToPrintWidth); + window.setTimeout(fitPlotToPrintWidth, 100); + }; + + const printMediaQuery = window.matchMedia?.('print'); + const handlePrintMediaChange = (event) => { + if (event.matches) { + handleBeforePrint(); + } else { + restorePlotWidth(); + } + }; + + window.addEventListener('beforeprint', handleBeforePrint); + window.addEventListener('afterprint', restorePlotWidth); + printMediaQuery?.addEventListener?.('change', handlePrintMediaChange); + + return () => { + window.removeEventListener('beforeprint', handleBeforePrint); + window.removeEventListener('afterprint', restorePlotWidth); + printMediaQuery?.removeEventListener?.('change', handlePrintMediaChange); + }; + }, [layout?.height, layout?.width, plotly]); + return ( { ...(autoscale ? { autosize: false } : {}), }} onInitialized={(...args) => { - if (ref) { - ref.current = args[1]; - } + setGraphDiv(args[1]); if (onInitialized) { onInitialized(...args); } diff --git a/src/less/plotly.less b/src/less/plotly.less index d698731..e883004 100644 --- a/src/less/plotly.less +++ b/src/less/plotly.less @@ -109,32 +109,3 @@ } } } - -@media print { - .visualization:not(.autoscale) { - .plot-container { - overflow: visible !important; - } - } - - .plotly-component, - .visualization, - .plot-container, - .svg-container, - .js-plotly-plot { - max-width: 100% !important; - break-inside: avoid; - page-break-inside: avoid; - } - - .svg-container, - .main-svg { - overflow: visible !important; - } - - .modebar, - .modebar-container, - .visualization-toolbar { - display: none !important; - } -}