Conversation
| #[serde(rename = "fig-width")] | ||
| pub fig_width: Option<f64>, | ||
|
|
||
| /// Figure height in inches, set by Quarto | ||
| #[serde(rename = "fig-height")] | ||
| pub fig_height: Option<f64>, | ||
|
|
||
| /// Output area width in pixels | ||
| pub output_width_px: Option<f64>, | ||
|
|
||
| /// Device pixel ratio of the output area | ||
| pub output_pixel_ratio: Option<f64>, |
There was a problem hiding this comment.
Although the dependency direction would be a little weird, I'd love if we could reuse PlotRenderSettings from the plot comm for this. Looks like we only need to add output_width_px.
At the very least, duplicating the data structure if we can't reuse it would mean that the plot settings are nicely namespaced in their own struct and keep the parent struct tidy.
There was a problem hiding this comment.
Yea at the very least it feels like this should be something like
pub plot_render_settings: Option<PlotRenderSettings>But if @lionel- is suggesting that perhaps this should not be a part of ExecuteRequestPositron at all, then I'd also be fine with that (I can't quite tell)
There was a problem hiding this comment.
It's fine in the execute request, just needs some consolidation as in your snippet.
There was a problem hiding this comment.
Nice! In this case though, I meant collecting all the plot setting fields in a nested struct in the execute_request.positron struct.
| let has_sizing = p.fig_width.is_some() || | ||
| p.fig_height.is_some() || | ||
| p.output_width_px.is_some(); | ||
| has_sizing.then_some(graphics_device::PlotSizingMetadata { | ||
| fig_width: p.fig_width, | ||
| fig_height: p.fig_height, | ||
| output_width_px: p.output_width_px, | ||
| output_pixel_ratio: p.output_pixel_ratio, | ||
| }) | ||
| }); |
There was a problem hiding this comment.
This is a bit domain-specific for this file. Perhaps make a from_execute_request() method on PlotSizingMetadata?
| ); | ||
|
|
||
| let settings = self.prerender_settings.get(); | ||
| // Use sizing metadata for prerender if available, otherwise fall back to default |
There was a problem hiding this comment.
Would be helpful to mention that we're merging any overrides available for this plot (possibly not exhaustive) into current frontend defaults.
There was a problem hiding this comment.
That feels kind of wrong to me? i.e. for plot specific metadata to affect global state in this way
There was a problem hiding this comment.
The global state is copied locally from the Cell, then the overrides are merged in the copy and used in the pre-render. Not global side effect IIUC.
| /// | ||
| /// Returns dimensions in CSS/logical pixels. The R rendering layer handles | ||
| /// physical pixel scaling via the separate `pixel_ratio` parameter. | ||
| fn intrinsic_size_to_plot_size(intrinsic: &IntrinsicSize) -> PlotSize { |
There was a problem hiding this comment.
IntrinsicSize belongs to Amalthea but you could make an extension trait IntrinsicSizeExt in Ark and make this IntrinsicSizeExt::to_plot_size()?
| /// If `fig_width`/`fig_height` are both set, returns an intrinsic size in inches | ||
| /// with source "Quarto". Otherwise returns `None` (output_width_px affects render | ||
| /// size but is not an intrinsic size). | ||
| fn intrinsic_size_from_metadata(sizing: &PlotSizingMetadata) -> Option<IntrinsicSize> { |
There was a problem hiding this comment.
PlotSizingMetadata::to_intrinsic_size()
There was a problem hiding this comment.
Decided to just use IntrinsicSize directly
| /// If `fig_width`/`fig_height` are set, converts inches to logical pixels. | ||
| /// If only `output_width_px` is set, uses it as width with a default aspect ratio. | ||
| /// Otherwise returns `None` (use the default prerender settings). | ||
| fn prerender_overrides_from_metadata(sizing: &PlotSizingMetadata) -> Option<(PlotSize, f64)> { |
There was a problem hiding this comment.
PlotSizingMetadata::to_prerender_overrides()
There was a problem hiding this comment.
Refactored out of PlotSizingMetadata
| metadata: RefCell<HashMap<PlotId, PlotMetadata>>, | ||
|
|
||
| /// Mapping of plot ID to its intrinsic size (set when Quarto specifies fig-width/fig-height) | ||
| intrinsic_sizes: RefCell<HashMap<PlotId, IntrinsicSize>>, |
There was a problem hiding this comment.
We've now got 3 hashmaps over PlotId. Is it time for a struct?
There was a problem hiding this comment.
Good question! The one that tracks sockets has a different lifetime, but I've combined the other two.
| /// When `fig_width`/`fig_height` are set, they take priority (Quarto-specified size in inches). | ||
| /// Otherwise, `output_width_px` is used as the render width. | ||
| #[derive(Debug, Clone)] | ||
| pub(crate) struct PlotSizingMetadata { |
There was a problem hiding this comment.
Yea this does feel very close to PlotRenderSettings!
There was a problem hiding this comment.
Removed in favor of PlotRenderSettings!
| Some(intrinsic) => Self::intrinsic_size_to_plot_size(&intrinsic), | ||
| None => { | ||
| return Err(anyhow!( | ||
| "Intrinsically sized plots are not yet supported." |
There was a problem hiding this comment.
Is that the right message? Or is it just that we somehow are missing an intrinsic size for this id?
There was a problem hiding this comment.
Good catch, it's the latter one. I've updated the message.
| } | ||
|
|
||
| /// Default DPI for converting inches to pixels (matches R's default on macOS). | ||
| const DEFAULT_DPI: f64 = 96.0; |
There was a problem hiding this comment.
FWIW, in graphics.R
#' Default OS resolution in PPI (pixels per inch)
#'
#' Thomas thinks these are "more correct than any other numbers." Specifically,
#' macOS uses 96 DPI for its internal scaling, but this is user definable on
#' Windows.
#'
#' This corresponds to a scaling factor that tries to make things that appear
#' "on screen" be as close to the size in which they are actually printed at,
#' which has always been tricky.
default_resolution_in_pixels_per_inch <- function() {
if (Sys.info()[["sysname"]] == "Darwin") {
96L
} else {
72L
}
}| width: w, | ||
| height: h, | ||
| unit: PlotUnit::Inches, | ||
| source: String::from("Quarto"), |
There was a problem hiding this comment.
Calling out that hardcoding Quarto doesn't feel quite right to me
There was a problem hiding this comment.
There's no other way for these values to be set AFAIK... I think the only way to avoid this would be to give this intrinsic size a generic name (like "document"? "external"?), or to add another field to the metadata naming the source of the figure size values that Positron could set. Does either of those seem right to you?
There was a problem hiding this comment.
So I'm looking at
pub struct IntrinsicSize {
/// The width of the plot
pub width: f64,
/// The height of the plot
pub height: f64,
/// The unit of measurement of the plot's dimensions
pub unit: PlotUnit,
/// The source of the intrinsic size e.g. 'Matplotlib'
pub source: String
}
and mentally I'm understanding source here as "the thing that provided this width/height/unit override"
So to me it would make sense for this proposed ExecutionMetadata to have a source?: String field that is "the source of all metadata overrides", which Quarto would set to itself when it calls session.execute() and would flow through to ark.
That feels kind of right to me, and may be useful for other things in the future?
If source is not set in ExecutionMetadata, then as a fallback you could probably say that the source for the IntrinsicSize is something like "Metadata", but I also feel like it would be kind of reasonable for IntrinsicSize's source field to be optional, and we'd just not provide anything at all if ExecutionMetadata lacks the source field but does happen to provide fig_height and fig_width. Sort of a recognition of "we got these things but we don't know where they originally came from". And if Positron displays this information somehow to the user, it would just not show the source field or would show ??? if it had to show something.
Does that make any sense?
| } | ||
|
|
||
| fn create_display_data_plot(&self, id: &PlotId) -> Result<serde_json::Value, anyhow::Error> { | ||
| // TODO: Take these from R global options? Like `ark.plot.width`? |
There was a problem hiding this comment.
This TODO should stay
There was a problem hiding this comment.
For example, how would, say, Jupyterlab or Zed set this?
There was a problem hiding this comment.
Well, while I'm in here I guess I could just make it read from global options :-) 11542ad
| return Some(( | ||
| PlotSize { | ||
| width: (w * Self::DEFAULT_DPI).round() as i64, | ||
| height: (h * Self::DEFAULT_DPI).round() as i64, |
There was a problem hiding this comment.
So, IIUC, your goal is to massage the sizing information into pixels for the R side of the graphics engine
#' ...
#' @param width The plot width, in pixels.
#' @param height The plot height, in pixels.
#' @param pixel_ratio The device pixel ratio (e.g. 1 for standard displays, 2
#' for retina displays)
#' ...
with_graphics_device <- function(...even if on the R side we possibly invert them back from pixels -> inches depending on the graphics device
finalize_device_arguments <- function(format, width, height, pixel_ratio) {
if (format == "png" || format == "jpeg" || format == "tiff") {
# These devices require `width` and `height` in pixels, which is what
# they are provided in already. For pixel based devices, all relevant
# values are upscaled by `pixel_ratio`.
#
# `res` is nominal resolution specified in pixels-per-inch (ppi).
return(list(
res = default_resolution_in_pixels_per_inch() * pixel_ratio,
width = width * pixel_ratio,
height = height * pixel_ratio
))
}
if (format == "svg" || format == "pdf") {
# These devices require `width` and `height` in inches, but they are
# provided to us in pixels, so we have to perform a conversion here.
# For vector based devices, providing the size in inches implicitly
# tells the device the relative size to use for things like text,
# since that is the absolute unit (pts are based on inches).
#
# Thomas says the math for `width` and `height` here are correct, i.e.
# we don't also multiply `default_resolution_in_pixels_per_inch()` by
# `pixel_ratio` like we do above, which would have made it cancel out of
# the equation below.
#
# There is no `res` argument for these devices.
return(list(
res = NULL,
width = width *
pixel_ratio /
default_resolution_in_pixels_per_inch(),
height = height *
pixel_ratio /
default_resolution_in_pixels_per_inch()
))
}
stop("Internal error: Unknown plot `format`.")
}I think that probably makes sense, but if that is the case I do think we should definitely make an attempt to keep default_resolution_in_pixels_per_inch() in sync with DEFAULT_DPI (could even give them the same name with a comment that links them together, or go all the way to making the R one just return the Rust variable)
There was a problem hiding this comment.
It does feel a LITTLE weird to go through inches -> px -> inches here! But don't think there's any arrangement of this that isn't awkward somewhere.
I've updated it so that Rust is the keeper of the default DPI setting and R fetches it from there.
| ); | ||
|
|
||
| let settings = self.prerender_settings.get(); | ||
| // Use sizing metadata for prerender if available, otherwise fall back to default |
There was a problem hiding this comment.
That feels kind of wrong to me? i.e. for plot specific metadata to affect global state in this way
| // PNG IHDR: 8-byte signature, 4-byte chunk length, 4-byte "IHDR", then width (4) and height (4) | ||
| let width = u32::from_be_bytes([bytes[16], bytes[17], bytes[18], bytes[19]]); | ||
| let height = u32::from_be_bytes([bytes[20], bytes[21], bytes[22], bytes[23]]); | ||
| (width, height) |
There was a problem hiding this comment.
You can thank Claude for this one, its initial run at test coverage for this feature was 200 lines of tests to be sure that DPI -> inch conversion was correct (yes Claude good job I am worried that someone might break the division operator in a future change).
I asked it to do a more meaningful higher-level test instead and it came up with the idea of checking the actual size of the emitted PNG which I would never have thought to have done!
lionel-
left a comment
There was a problem hiding this comment.
Looks good! but would be nice to collect plot settings in a struct nested in the positron extension of execute_request, instead of the flat structure.
| #[serde(rename = "fig-width")] | ||
| pub fig_width: Option<f64>, | ||
|
|
||
| /// Figure height in inches, set by Quarto | ||
| #[serde(rename = "fig-height")] | ||
| pub fig_height: Option<f64>, | ||
|
|
||
| /// Output area width in pixels | ||
| pub output_width_px: Option<f64>, | ||
|
|
||
| /// Device pixel ratio of the output area | ||
| pub output_pixel_ratio: Option<f64>, |
There was a problem hiding this comment.
Nice! In this case though, I meant collecting all the plot setting fields in a nested struct in the execute_request.positron struct.
|
Yes, that'd be nicer! But Positron itself doesn't necessarily know about all these values; it allows extensions to inject additional metadata, and e.g. |
There was a problem hiding this comment.
Hopefully it shows you my response to your comment about IntrinsicSize's source #1119 (comment)
| pub struct ExecuteRequestPositron { | ||
| pub code_location: Option<JupyterPositronLocation>, | ||
|
|
||
| /// Figure width in inches, set by Quarto | ||
| #[serde(rename = "fig-width")] | ||
| pub fig_width: Option<f64>, | ||
|
|
||
| /// Figure height in inches, set by Quarto | ||
| #[serde(rename = "fig-height")] | ||
| pub fig_height: Option<f64>, | ||
|
|
||
| /// Output area width in pixels | ||
| pub output_width_px: Option<f64>, | ||
|
|
||
| /// Device pixel ratio of the output area | ||
| pub output_pixel_ratio: Option<f64>, |
There was a problem hiding this comment.
I believe I would be ok with keeping these nested 1 layer deeper under execution_metadata: Option<ExecutionMetadata>, mirroring your initial implementation here posit-dev/positron#12513
IIUC, we could frame it as:
code_locationis "officially supported" by Positron and is Positron providedexecution_metadatais "passed through" by Positron and may contain fields that any extension feels like passing through in theirsession.execute()calls.
On the Ark side, we'd just "happen to recognize" some specific execution_metadata fields of fig_width, fig_height, etc, and ignore (probably?) anything else that comes over the wire that we don't recognize.
But I like the idea of containing these extension-specific-fields-that-ark-happens-to-recognize scoped to execution_metadata
| width: w, | ||
| height: h, | ||
| unit: PlotUnit::Inches, | ||
| source: String::from("Quarto"), |
There was a problem hiding this comment.
So I'm looking at
pub struct IntrinsicSize {
/// The width of the plot
pub width: f64,
/// The height of the plot
pub height: f64,
/// The unit of measurement of the plot's dimensions
pub unit: PlotUnit,
/// The source of the intrinsic size e.g. 'Matplotlib'
pub source: String
}
and mentally I'm understanding source here as "the thing that provided this width/height/unit override"
So to me it would make sense for this proposed ExecutionMetadata to have a source?: String field that is "the source of all metadata overrides", which Quarto would set to itself when it calls session.execute() and would flow through to ark.
That feels kind of right to me, and may be useful for other things in the future?
If source is not set in ExecutionMetadata, then as a fallback you could probably say that the source for the IntrinsicSize is something like "Metadata", but I also feel like it would be kind of reasonable for IntrinsicSize's source field to be optional, and we'd just not provide anything at all if ExecutionMetadata lacks the source field but does happen to provide fig_height and fig_width. Sort of a recognition of "we got these things but we don't know where they originally came from". And if Positron displays this information somehow to the user, it would just not show the source field or would show ??? if it had to show something.
Does that make any sense?
This change adds the ability to change the way plots are drawn using four new values in the
positronfield ofexecute_requestfig-width/fig-heightfrom Quarto (takes precedence), from the cell optionsoutput_width_pxandoutput_pixel_ratiofrom the notebook and/or source editor. These are device metrics that can change at any time.Used together, these values cause most plots in notebooks and Quarto documents to be drawn at the user's preferred size, and at the scaling appropriate for their display.
Quarto:
Jupyter: