Skip to content

Zoom plots in editor #8126

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 13 commits into from
Jun 25, 2025
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,12 @@ import { ProgressBar } from '../../../../../base/browser/ui/progressbar/progress
import { DisposableStore } from '../../../../../base/common/lifecycle.js';
import { localize } from '../../../../../nls.js';
import { PanZoomImage } from './panZoomImage.js';
import { ZoomLevel } from './zoomPlotMenuButton.js';
import { usePositronPlotsContext } from '../positronPlotsContext.js';
import { PlotClientInstance, PlotClientState } from '../../../../services/languageRuntime/common/languageRuntimePlotClient.js';
import { IPositronPlotSizingPolicy } from '../../../../services/positronPlots/common/sizingPolicy.js';
import { PlotSizingPolicyAuto } from '../../../../services/positronPlots/common/sizingPolicyAuto.js';
import { PlotSizingPolicyIntrinsic } from '../../../../services/positronPlots/common/sizingPolicyIntrinsic.js';
import { ZoomLevel } from '../../../../services/positronPlots/common/positronPlots.js';

/**
* DynamicPlotInstanceProps interface.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import React from 'react';

// Other dependencies.
import { Scrollable } from '../../../../../base/browser/ui/positronComponents/scrollable/Scrollable.js';
import { ZoomLevel } from './zoomPlotMenuButton.js';
import { ZoomLevel } from '../../../../services/positronPlots/common/positronPlots.js';

interface PanZoomImageProps {
width: number;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import { WebviewPlotThumbnail } from './webviewPlotThumbnail.js';
import { usePositronPlotsContext } from '../positronPlotsContext.js';
import { WebviewPlotClient } from '../webviewPlotClient.js';
import { PlotClientInstance } from '../../../../services/languageRuntime/common/languageRuntimePlotClient.js';
import { DarkFilter, IPositronPlotClient, IPositronPlotsService, PlotRenderFormat } from '../../../../services/positronPlots/common/positronPlots.js';
import { DarkFilter, IPositronPlotClient, IPositronPlotsService, isZoomablePlotClient, PlotRenderFormat, ZoomLevel } from '../../../../services/positronPlots/common/positronPlots.js';
import { StaticPlotClient } from '../../../../services/positronPlots/common/staticPlotClient.js';
import { PlotSizingPolicyIntrinsic } from '../../../../services/positronPlots/common/sizingPolicyIntrinsic.js';
import { PlotSizingPolicyAuto } from '../../../../services/positronPlots/common/sizingPolicyAuto.js';
Expand All @@ -39,7 +39,6 @@ interface PlotContainerProps {
visible: boolean;
showHistory: boolean;
darkFilterMode: DarkFilter;
zoom: number;
}

/**
Expand All @@ -59,6 +58,7 @@ export const PlotsContainer = (props: PlotContainerProps) => {
const positronPlotsContext = usePositronPlotsContext();
const plotHistoryRef = React.createRef<HTMLDivElement>();
const containerRef = useRef<HTMLDivElement>(undefined!);
const [zoom, setZoom] = React.useState<ZoomLevel>(ZoomLevel.Fit);

// We generally prefer showing the plot history on the bottom (making the
// plot wider), but if the plot container is too wide, we show it on the
Expand Down Expand Up @@ -165,6 +165,29 @@ export const PlotsContainer = (props: PlotContainerProps) => {
};
}, [plotWidth, plotHeight, props.positronPlotsService]);

useEffect(() => {
// Create the disposable store for cleanup.
const disposableStore = new DisposableStore();

// Get the current plot instance using the selected instance ID from the
// PositronPlotsContext.
const currentPlotInstance = positronPlotsContext.positronPlotInstances.find(
(plotInstance) => plotInstance.id === positronPlotsContext.selectedInstanceId
);
if (currentPlotInstance && isZoomablePlotClient(currentPlotInstance)) {
// Listen to the plot instance for zoom level changes.
disposableStore.add(currentPlotInstance.onDidChangeZoomLevel((zoomLevel) => {
setZoom(zoomLevel);
}));
// Set the initial zoom level.
setZoom(currentPlotInstance.zoomLevel);
}
return () => {
// Dispose of the disposable store when the component unmounts.
disposableStore.dispose();
}
}, [positronPlotsContext.positronPlotInstances, positronPlotsContext.selectedInstanceId]);

/**
* Renders either a DynamicPlotInstance (resizable plot), a
* StaticPlotInstance (static plot image), or a WebviewPlotInstance
Expand All @@ -180,12 +203,12 @@ export const PlotsContainer = (props: PlotContainerProps) => {
height={plotHeight}
plotClient={plotInstance}
width={plotWidth}
zoom={props.zoom} />;
zoom={zoom} />;
} else if (plotInstance instanceof StaticPlotClient) {
return <StaticPlotInstance
key={plotInstance.id}
plotClient={plotInstance}
zoom={props.zoom} />;
zoom={zoom} />;
} else if (plotInstance instanceof WebviewPlotClient) {
return <WebviewPlotInstance
key={plotInstance.id}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ import React, { useEffect, useRef, useState } from 'react';

// Other dependencies.
import { PanZoomImage } from './panZoomImage.js';
import { ZoomLevel } from './zoomPlotMenuButton.js';
import { StaticPlotClient } from '../../../../services/positronPlots/common/staticPlotClient.js';
import { ZoomLevel } from '../../../../services/positronPlots/common/positronPlots.js';

/**
* StaticPlotInstanceProps interface.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,21 +11,14 @@ import * as nls from '../../../../../nls.js';
import { IAction } from '../../../../../base/common/actions.js';
import { ActionBarMenuButton } from '../../../../../platform/positronActionBar/browser/components/actionBarMenuButton.js';
import { ThemeIcon } from '../../../../../base/common/themables.js';

export enum ZoomLevel {
Fit = 0,
Fifty = 0.5,
SeventyFive = 0.75,
OneHundred = 1,
TwoHundred = 2,
}
import { ZoomLevel } from '../../../../services/positronPlots/common/positronPlots.js';

interface ZoomPlotMenuButtonProps {
readonly actionHandler: (zoomLevel: ZoomLevel) => void;
readonly zoomLevel: number;
}

const zoomLevelMap = new Map<ZoomLevel, string>([
export const zoomLevelMap = new Map<ZoomLevel, string>([
[ZoomLevel.Fit, nls.localize('positronZoomFit', 'Fit')],
[ZoomLevel.Fifty, nls.localize('positronZoomFifty', '50%')],
[ZoomLevel.SeventyFive, nls.localize('positronZoomSeventyFive', '75%')],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,13 @@ import { PositronPlotsService } from './positronPlotsService.js';
import { IPositronPlotsService, POSITRON_PLOTS_VIEW_ID } from '../../../services/positronPlots/common/positronPlots.js';
import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions, IWorkbenchContribution } from '../../../common/contributions.js';
import { Extensions as ViewContainerExtensions, IViewsRegistry } from '../../../common/views.js';
import { registerAction2 } from '../../../../platform/actions/common/actions.js';
import { PlotsActiveEditorCopyAction, PlotsActiveEditorSaveAction, PlotsClearAction, PlotsCopyAction, PlotsEditorAction, PlotsNextAction, PlotsPopoutAction, PlotsPreviousAction, PlotsRefreshAction, PlotsSaveAction, PlotsSizingPolicyAction } from './positronPlotsActions.js';
import { MenuRegistry, registerAction2, MenuId, ISubmenuItem } from '../../../../platform/actions/common/actions.js';
import { PlotsActiveEditorCopyAction, PlotsActiveEditorSaveAction, PlotsClearAction, PlotsEditorZoomAction, PlotsCopyAction, PlotsEditorAction, PlotsNextAction, PlotsPopoutAction, PlotsPreviousAction, PlotsRefreshAction, PlotsSaveAction, PlotsSizingPolicyAction, ZoomFiftyAction, ZoomOneHundredAction, ZoomSeventyFiveAction, ZoomToFitAction, ZoomTwoHundredAction } from './positronPlotsActions.js';
import { POSITRON_SESSION_CONTAINER } from '../../positronSession/browser/positronSessionContainer.js';
import { Extensions as ConfigurationExtensions, IConfigurationRegistry } from '../../../../platform/configuration/common/configurationRegistry.js';
import { localize } from '../../../../nls.js';
import { localize, localize2 } from '../../../../nls.js';
import { FreezeSlowPlotsConfigKey } from '../../../services/languageRuntime/common/languageRuntimePlotClient.js';
import { PLOT_IS_ACTIVE_EDITOR } from '../../positronPlotsEditor/browser/positronPlotsEditor.contribution.js';

// Register the Positron plots service.
registerSingleton(IPositronPlotsService, PositronPlotsService, InstantiationType.Delayed);
Expand Down Expand Up @@ -76,6 +77,27 @@ class PositronPlotsContribution extends Disposable implements IWorkbenchContribu
registerAction2(PlotsActiveEditorCopyAction);
registerAction2(PlotsActiveEditorSaveAction);
registerAction2(PlotsSizingPolicyAction);
this.registerEditorZoomSubMenu();
}

private registerEditorZoomSubMenu(): void {
// Register the main submenu for the editor action bar
const zoomSubmenu: ISubmenuItem = {
title: localize2('positronPlots.zoomSubMenuTitle', 'Set the plot zoom'),
submenu: PlotsEditorZoomAction.SUBMENU_ID,
when: PLOT_IS_ACTIVE_EDITOR,
group: 'navigation',
order: 3,
icon: Codicon.positronSizeToFit
};
MenuRegistry.appendMenuItem(MenuId.EditorActionsLeft, zoomSubmenu);

// Register all the zoom actions
registerAction2(ZoomToFitAction);
registerAction2(ZoomFiftyAction);
registerAction2(ZoomSeventyFiveAction);
registerAction2(ZoomOneHundredAction);
registerAction2(ZoomTwoHundredAction);
}
}

Expand Down
49 changes: 44 additions & 5 deletions src/vs/workbench/contrib/positronPlots/browser/positronPlots.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,12 @@ import React, { PropsWithChildren, useCallback, useEffect, useState } from 'reac
import { IWorkbenchLayoutService } from '../../../services/layout/browser/layoutService.js';
import { PositronPlotsServices } from './positronPlotsState.js';
import { PositronPlotsContextProvider } from './positronPlotsContext.js';
import { HistoryPolicy, IPositronPlotsService } from '../../../services/positronPlots/common/positronPlots.js';
import { HistoryPolicy, IPositronPlotsService, isZoomablePlotClient, ZoomLevel } from '../../../services/positronPlots/common/positronPlots.js';
import { DisposableStore } from '../../../../base/common/lifecycle.js';
import { PlotsContainer } from './components/plotsContainer.js';
import { ActionBars } from './components/actionBars.js';
import { INotificationService } from '../../../../platform/notification/common/notification.js';
import { PositronPlotsViewPane } from './positronPlotsView.js';
import { ZoomLevel } from './components/zoomPlotMenuButton.js';
import { IPreferencesService } from '../../../services/preferences/common/preferences.js';

/**
Expand Down Expand Up @@ -66,7 +65,16 @@ export const PositronPlots = (props: PropsWithChildren<PositronPlotsProps>) => {
}, [props.positronPlotsService.positronPlotInstances.length, props.reactComponentContainer.height, props.reactComponentContainer.width]);

const zoomHandler = (zoom: number) => {
setZoom(zoom);
const currentPlotId = props.positronPlotsService.selectedPlotId;
if (!currentPlotId) {
return;
}

const plot = props.positronPlotsService.positronPlotInstances.find(plot => plot.id === currentPlotId);
if (isZoomablePlotClient(plot)) {
// Update the zoom level in the plot metadata.
plot.zoomLevel = zoom;
}
};

// Hooks.
Expand Down Expand Up @@ -129,10 +137,41 @@ export const PositronPlots = (props: PropsWithChildren<PositronPlotsProps>) => {
return () => disposableStore.dispose();
}, [computeHistoryVisibility, props.positronPlotsService, props.reactComponentContainer]);

useEffect(() => {
// Set the initial zoom level for the current plot.
const disposableStore = new DisposableStore();

disposableStore.add(props.positronPlotsService.onDidSelectPlot(plotId => {
const currentPlot = props.positronPlotsService.selectedPlotId;

if (currentPlot) {
const plot = props.positronPlotsService.positronPlotInstances.find(plot => plot.id === currentPlot);
if (isZoomablePlotClient(plot)) {
disposableStore.add(plot.onDidChangeZoomLevel((zoomLevel) => {
setZoom(zoomLevel);
}));
setZoom(plot.zoomLevel);
} else {
setZoom(ZoomLevel.Fit);
}
}
}));

return () => {
// Dispose of the disposable store to clean up event handlers.
disposableStore.dispose();
}
}, [props.positronPlotsService]);

// Render.
return (
<PositronPlotsContextProvider {...props}>
<ActionBars {...props} zoomHandler={zoomHandler} zoomLevel={zoom} />
<ActionBars
{...props}
key={props.positronPlotsService.selectedPlotId}
zoomHandler={zoomHandler}
zoomLevel={zoom}
/>
<PlotsContainer
darkFilterMode={darkFilterMode}
height={height > 0 ? height - 34 : 0}
Expand All @@ -142,7 +181,7 @@ export const PositronPlots = (props: PropsWithChildren<PositronPlotsProps>) => {
width={width}
x={posX}
y={posY}
zoom={zoom} />
/>
</PositronPlotsContextProvider>
);

Expand Down
Loading