Skip to content

Commit

Permalink
Create plot client in backend
Browse files Browse the repository at this point in the history
Opens another comm to a plot in the backend
Implement Python plots in editor for the frontend
  • Loading branch information
timtmok committed Aug 29, 2024
1 parent 0739250 commit 5da4fa8
Show file tree
Hide file tree
Showing 16 changed files with 275 additions and 55 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import io
import logging
from typing import Optional, Union, cast
import uuid

import matplotlib
from matplotlib.backend_bases import FigureManagerBase
Expand Down Expand Up @@ -61,7 +62,8 @@ def __init__(self, canvas: FigureCanvasPositron, num: Union[int, str]):

# Create the plot instance via the plots service.
self._plots_service = cast(PositronIPyKernel, PositronIPyKernel.instance()).plots_service
self._plot = self._plots_service.create_plot(canvas.render, canvas.intrinsic_size)
self._plot_id = str(uuid.uuid4())
self._plot = self._plots_service.create_plot(canvas.render, canvas.intrinsic_size, self._plot_id)

@property
def closed(self) -> bool:
Expand All @@ -71,21 +73,27 @@ def show(self) -> None:
"""
Called by matplotlib when a figure is shown via `plt.show()` or `figure.show()`.
"""
self._plot.show()
plot_clients = self._plots_service.get_plot_clients(self._plot_id)
for plot_client in plot_clients:
plot_client.show()

def destroy(self) -> None:
"""
Called by matplotlib when a figure is closed via `plt.close()`.
"""
self._plot.close()
plot_clients = self._plots_service.get_plot_clients(self._plot_id)
for plot_client in plot_clients:
plot_client.close()

def update(self) -> None:
"""
Notify the frontend that the plot needs to be rerendered.
Called by the canvas when a figure is drawn and its contents have changed.
"""
self._plot.update()
plot_clients = self._plots_service.get_plot_clients(self._plot_id)
for plot_client in plot_clients:
plot_client.update()


class FigureCanvasPositron(FigureCanvasAgg):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,17 @@ class PlotUnit(str, enum.Enum):
Inches = "inches"


@enum.unique
class PlotClientView(str, enum.Enum):
"""
Possible values for PlotClientView
"""

View = "view"

Editor = "editor"


class IntrinsicSize(BaseModel):
"""
The intrinsic size of a plot, if known
Expand Down Expand Up @@ -100,13 +111,47 @@ class PlotBackendRequest(str, enum.Enum):
An enumeration of all the possible requests that can be sent to the backend plot comm.
"""

# Create a new plot client
CreateNewPlotClient = "create_new_plot_client"

# Get the intrinsic size of a plot, if known.
GetIntrinsicSize = "get_intrinsic_size"

# Render a plot
Render = "render"


class CreateNewPlotClientParams(BaseModel):
"""
Creates a new plot client based on the existing plot client. The new
client will be backed by the same plot.
"""

client_view: PlotClientView = Field(
description="The location the client intends to show the plot",
)


class CreateNewPlotClientRequest(BaseModel):
"""
Creates a new plot client based on the existing plot client. The new
client will be backed by the same plot.
"""

params: CreateNewPlotClientParams = Field(
description="Parameters to the CreateNewPlotClient method",
)

method: Literal[PlotBackendRequest.CreateNewPlotClient] = Field(
description="The JSON-RPC method name (create_new_plot_client)",
)

jsonrpc: str = Field(
default="2.0",
description="The JSON-RPC version specifier",
)


class GetIntrinsicSizeRequest(BaseModel):
"""
The intrinsic size of a plot is the size at which a plot would be if
Expand Down Expand Up @@ -166,6 +211,7 @@ class RenderRequest(BaseModel):
class PlotBackendMessageContent(BaseModel):
comm_id: str
data: Union[
CreateNewPlotClientRequest,
GetIntrinsicSizeRequest,
RenderRequest,
] = Field(..., discriminator="method")
Expand All @@ -190,6 +236,10 @@ class PlotFrontendEvent(str, enum.Enum):

PlotSize.update_forward_refs()

CreateNewPlotClientParams.update_forward_refs()

CreateNewPlotClientRequest.update_forward_refs()

GetIntrinsicSizeRequest.update_forward_refs()

RenderParams.update_forward_refs()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,14 @@
import base64
import logging
import uuid
from typing import List, Optional, Protocol, Tuple
from typing import Dict, List, Optional, Protocol, Tuple, Union, cast

from .plot_comm import (
CreateNewPlotClientRequest,
GetIntrinsicSizeRequest,
IntrinsicSize,
PlotBackendMessageContent,
PlotClientView,
PlotFrontendEvent,
PlotResult,
PlotSize,
Expand Down Expand Up @@ -41,6 +43,8 @@ class Plot:
Paramaters
----------
plot_id
The unique identifier of the backend plot.
comm
The communication channel to the frontend plot instance.
render
Expand All @@ -51,13 +55,17 @@ class Plot:

def __init__(
self,
plot_id: str,
comm: PositronComm,
render: Renderer,
intrinsic_size: Tuple[int, int],
client_view: PlotClientView,
) -> None:
self.plot_id = plot_id
self._comm = comm
self._render = render
self._intrinsic_size = intrinsic_size
self._client_view = client_view

self._closed = False

Expand All @@ -78,7 +86,7 @@ def _open(self) -> None:
if not self._closed:
return

self._comm.open()
self._comm.open({"clientView": self._client_view})
self._closed = False

def close(self) -> None:
Expand Down Expand Up @@ -120,6 +128,8 @@ def _handle_msg(self, msg: CommMessage[PlotBackendMessageContent], raw_msg: Json
)
if isinstance(request, GetIntrinsicSizeRequest):
self._handle_get_intrinsic_size()
if isinstance(request, CreateNewPlotClientRequest):
self._handle_create_new_plot_client(request.params.client_view)
else:
logger.warning(f"Unhandled request: {request}")

Expand All @@ -146,6 +156,11 @@ def _handle_get_intrinsic_size(self) -> None:
).dict()
self._comm.send_result(data=result)

def _handle_create_new_plot_client(self, client_view: str) -> None:
from .positron_ipkernel import PositronIPyKernel
plots_service = cast(PositronIPyKernel, PositronIPyKernel.instance()).plots_service
plots_service.create_plot(self._render, self._intrinsic_size, self.plot_id, client_view)

def _handle_close(self, msg: JsonRecord) -> None:
self.close()

Expand Down Expand Up @@ -174,9 +189,25 @@ def __init__(self, target_name: str, session_mode: SessionMode):
self._target_name = target_name
self._session_mode = session_mode

self._plots: List[Plot] = []
self._plots: Dict[str, List[Plot]] = {}

def get_plot_clients(self, plot_id: str) -> List[Plot]:
"""
Get all the plot clients for a plot.
Parameters
----------
plot_id
The unique identifier of the plot.
Returns
-------
List[Plot]
The plot clients.
"""
return self._plots.get(plot_id, [])

def create_plot(self, render: Renderer, intrinsic_size: Tuple[int, int]) -> Plot:
def create_plot(self, render: Renderer, intrinsic_size: Tuple[int, int], plot_id: str, client_view: PlotClientView = PlotClientView.View) -> Plot:
"""
Create a plot.
Expand All @@ -186,22 +217,33 @@ def create_plot(self, render: Renderer, intrinsic_size: Tuple[int, int]) -> Plot
A callable that renders the plot. See `plot_comm.RenderRequest` for parameter details.
intrinsic_size
The intrinsic size of the plot in inches.
plot_id
The plot id.
client_view
The type of plot to create. If not provided, the plot will be created as a view plot.
See Also
--------
Plot
"""
comm_id = str(uuid.uuid4())
logger.info(f"Creating plot with comm {comm_id}")
plot_comm = PositronComm.create(self._target_name, comm_id)
plot = Plot(plot_comm, render, intrinsic_size)
self._plots.append(plot)

plot_comm = PositronComm.create(self._target_name, comm_id, {"clientView": client_view})
plot = Plot(plot_id, plot_comm, render, intrinsic_size, client_view)
plot_clients = self._plots.get(plot_id)

if (plot_clients is None):
plot_clients = []
self._plots[plot_id] = plot_clients
plot_clients.append(plot)
return plot

def shutdown(self) -> None:
"""
Shutdown the plots service.
"""
for plot in list(self._plots):
plot.close()
self._plots.remove(plot)
for plot_clients in self._plots.values():
for plot_client in plot_clients:
plot_client.close()
self._plots.clear()
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

import enum
import logging
from typing import Callable, Generic, Optional, Type, TypeVar
from typing import Any, Callable, Dict, Generic, Optional, Type, TypeVar

import comm

Expand Down Expand Up @@ -88,7 +88,7 @@ def __init__(self, comm: comm.base_comm.BaseComm) -> None:
self.comm = comm

@classmethod
def create(cls, target_name: str, comm_id: str) -> PositronComm:
def create(cls, target_name: str, comm_id: str, metadata: Dict[str, Any] = None) -> PositronComm:
"""
Create a Positron comm.
Expand All @@ -104,7 +104,7 @@ def create(cls, target_name: str, comm_id: str) -> PositronComm:
PositronComm
The new PositronComm instance.
"""
base_comm = comm.create_comm(target_name=target_name, comm_id=comm_id)
base_comm = comm.create_comm(target_name=target_name, comm_id=comm_id, metadata=metadata)
return cls(base_comm)

@property
Expand Down Expand Up @@ -257,8 +257,8 @@ def close(self) -> None:
"""
self.comm.close()

def open(self) -> None:
def open(self, metadata: Dict[str, Any] = None) -> None:
"""
Open the frontend-side version of this comm.
"""
self.comm.open()
self.comm.open(metadata=metadata)
24 changes: 24 additions & 0 deletions positron/comms/plot-backend-openrpc.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,25 @@
"version": "1.0.0"
},
"methods": [
{
"name": "create_new_plot_client",
"summary": "Create a new plot client",
"description": "Creates a new plot client based on the existing plot client. The new client will be backed by the same plot.",
"params": [
{
"name": "client_view",
"description": "The location the client intends to show the plot",
"schema": {
"$ref": "#/components/schemas/plot_client_view"
}
}
],
"result": {
"schema": {
"type": "null"
}
}
},
{
"name": "get_intrinsic_size",
"summary": "Get the intrinsic size of a plot, if known.",
Expand Down Expand Up @@ -120,6 +139,11 @@
"type": "string",
"description": "The unit of measurement of a plot's dimensions",
"enum": ["pixels", "inches"]
},
"plot_client_view": {
"type": "string",
"description": "The type of plot client",
"enum": ["view", "editor"]
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,7 @@ export const ActionBars = (props: PropsWithChildren<ActionBarsProps>) => {
ariaLabel={localize('positron-open-plot-editor', "Open plot in editor")}
onPressed={() => {
if (hasPlots) {
positronPlotsContext.positronPlotsService.openEditor();
positronPlotsContext.positronPlotsService.createEditorPlotClient();
}
}} />
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { ActionBars } from 'vs/workbench/contrib/positronPlots/browser/component
import { INotificationService } from 'vs/platform/notification/common/notification';
import { PositronPlotsViewPane } from 'vs/workbench/contrib/positronPlots/browser/positronPlotsView';
import { ZoomLevel } from 'vs/workbench/contrib/positronPlots/browser/components/zoomPlotMenuButton';
import { PlotClientView } from 'vs/workbench/services/languageRuntime/common/positronPlotComm';

/**
* PositronPlotsProps interface.
Expand Down Expand Up @@ -44,7 +45,7 @@ export const PositronPlots = (props: PropsWithChildren<PositronPlotsProps>) => {
return false;
case HistoryPolicy.Automatic:
// Don't show the history if there aren't at least two plots.
if (props.positronPlotsService.positronPlotInstances.length < 2) {
if (props.positronPlotsService.positronPlotInstances.filter(p => p.metadata.client_view === PlotClientView.View).length < 2) {
return false;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -228,7 +228,7 @@ export class PlotsEditorAction extends Action2 {
async run(accessor: ServicesAccessor) {
const plotsService = accessor.get(IPositronPlotsService);
if (plotsService.selectedPlotId) {
plotsService.openEditor();
plotsService.createEditorPlotClient();
} else {
accessor.get(INotificationService).info(localize('positronPlots.noPlotSelected', 'No plot selected.'));
}
Expand Down
Loading

0 comments on commit 5da4fa8

Please sign in to comment.