Skip to content

Commit 13cb4f4

Browse files
committed
add support for user-specified notebook plot dimensions
1 parent 7148b50 commit 13cb4f4

File tree

8 files changed

+147
-40
lines changed

8 files changed

+147
-40
lines changed

src/cpp/session/modules/NotebookPlots.R

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,15 @@
1515

1616
# creates the notebook graphics device
1717
.rs.addFunction("createNotebookGraphicsDevice", function(filename,
18-
width, pixelRatio, extraArgs)
18+
height, width, units, pixelRatio, extraArgs)
1919
{
20+
# form the arguments to the graphics device creator
2021
require(grDevices, quietly = TRUE)
2122
args <- list(
2223
filename = filename,
2324
width = width * pixelRatio,
24-
height = (width * pixelRatio) / 1.618,
25-
units = "px",
25+
height = height * pixelRatio,
26+
units = units,
2627
res = 96 * pixelRatio)
2728

2829
if (nchar(extraArgs) > 0)
@@ -71,7 +72,8 @@
7172
# output from the device
7273
output <- paste(tools::file_path_sans_ext(snapshot), "resized.png",
7374
sep = ".")
74-
.rs.createNotebookGraphicsDevice(output, width, pixelRatio, extraArgs);
75+
.rs.createNotebookGraphicsDevice(output, width / 1.618, width,
76+
"px", pixelRatio, extraArgs);
7577

7678
# actually replay the plot onto the device
7779
tryCatch({

src/cpp/session/modules/rmarkdown/NotebookExec.cpp

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,11 +60,12 @@ FilePath getNextOutputFile(const std::string& docId, const std::string& chunkId,
6060
} // anonymous namespace
6161

6262
ChunkExecContext::ChunkExecContext(const std::string& docId,
63-
const std::string& chunkId, int execScope, const std::string& options,
63+
const std::string& chunkId, int execScope, const json::Object& options,
6464
int pixelWidth, int charWidth):
6565
docId_(docId),
6666
chunkId_(chunkId),
6767
prevWorkingDir_(""),
68+
options_(options),
6869
pixelWidth_(pixelWidth),
6970
charWidth_(charWidth),
7071
prevCharWidth_(0),
@@ -113,12 +114,29 @@ void ChunkExecContext::connect()
113114
if (execScope_ == kExecScopeChunk)
114115
initializeOutput();
115116

117+
// extract knitr figure options if present
118+
double figWidth = 0;
119+
double figHeight = 0;
120+
json::readObject(options_, "fig.width", &figWidth);
121+
json::readObject(options_, "fig.height", &figHeight);
122+
116123
// begin capturing plots
117124
connections_.push_back(events().onPlotOutput.connect(
118125
boost::bind(&ChunkExecContext::onFileOutput, this, _1, _2,
119126
kChunkOutputPlot)));
120127

121-
error = beginPlotCapture(pixelWidth_, outputPath_);
128+
if (figWidth > 0 || figHeight > 0)
129+
{
130+
// user specified plot size, use it
131+
error = beginPlotCapture(figHeight, figWidth, PlotSizeManual,
132+
outputPath_);
133+
}
134+
else
135+
{
136+
// user didn't specify plot size, use the width of the editor surface
137+
error = beginPlotCapture(0, pixelWidth_, PlotSizeAutomatic,
138+
outputPath_);
139+
}
122140
if (error)
123141
LOG_ERROR(error);
124142

src/cpp/session/modules/rmarkdown/NotebookExec.hpp

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ class ChunkExecContext
4242
public:
4343
// initialize a new execution context
4444
ChunkExecContext(const std::string& docId, const std::string& chunkId,
45-
int execScope, const std::string& options, int pixelWidth,
45+
int execScope, const core::json::Object& options, int pixelWidth,
4646
int charWidth);
4747
~ChunkExecContext();
4848

@@ -74,6 +74,7 @@ class ChunkExecContext
7474
std::string prevWorkingDir_;
7575
std::string pendingInput_;
7676
core::FilePath outputPath_;
77+
core::json::Object options_;
7778

7879
int pixelWidth_;
7980
int charWidth_;

src/cpp/session/modules/rmarkdown/NotebookOutput.cpp

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
#include "SessionRmdNotebook.hpp"
1717
#include "NotebookCache.hpp"
1818
#include "NotebookOutput.hpp"
19+
#include "NotebookPlots.hpp"
1920

2021
#include <boost/foreach.hpp>
2122
#include <boost/format.hpp>
@@ -152,9 +153,22 @@ Error fillOutputObject(const std::string& docId, const std::string& chunkId,
152153
else if (outputType == kChunkOutputPlot || outputType == kChunkOutputHtml)
153154
{
154155
// plot/HTML outputs should be requested by the client, so pass the path
155-
(*pObj)[kChunkOutputValue] = kChunkOutputPath "/" + nbCtxId + "/" +
156-
docId + "/" + chunkId + "/" +
157-
path.filename();
156+
std::string url(kChunkOutputPath "/" + nbCtxId + "/" +
157+
docId + "/" + chunkId + "/" +
158+
path.filename());
159+
160+
// if this is a plot and it doesn't have a display list, hint to client
161+
// that plot can't be resized
162+
if (outputType == kChunkOutputPlot && path.hasExtensionLowerCase(".png"))
163+
{
164+
// form the path to where we'd expect the snapshot to be
165+
FilePath snapshotPath = path.parent().complete(
166+
path.stem() + kDisplayListExt);
167+
if (!snapshotPath.exists())
168+
url.append("?fixed_size=1");
169+
}
170+
171+
(*pObj)[kChunkOutputValue] = url;
158172
}
159173

160174
return Success();

src/cpp/session/modules/rmarkdown/NotebookPlots.cpp

Lines changed: 52 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
#include <r/session/RGraphics.hpp>
3333

3434
#define kPlotPrefix "_rs_chunk_plot_"
35+
#define kGoldenRatio 1.618
3536

3637
using namespace rstudio::core;
3738

@@ -45,14 +46,16 @@ namespace {
4546
class PlotState
4647
{
4748
public:
48-
PlotState(const FilePath& folder):
49+
PlotState(const FilePath& folder, PlotSizeBehavior sizeBehavior):
4950
plotFolder(folder),
50-
hasPlots(false)
51+
hasPlots(false),
52+
sizeBehavior(sizeBehavior)
5153
{
5254
}
5355

5456
FilePath plotFolder;
5557
bool hasPlots;
58+
PlotSizeBehavior sizeBehavior;
5659
FilePath snapshotFile;
5760
r::sexp::PreservedSEXP sexpMargins;
5861
boost::signals::connection onConsolePrompt;
@@ -143,7 +146,9 @@ void removeGraphicsDevice(boost::shared_ptr<PlotState> pPlotState)
143146
{
144147
// take a snapshot of the last plot's display list before we turn off the
145148
// device (if we haven't emitted it yet)
146-
if (pPlotState->hasPlots && pPlotState->snapshotFile.empty())
149+
if (pPlotState->hasPlots &&
150+
pPlotState->sizeBehavior == PlotSizeAutomatic &&
151+
pPlotState->snapshotFile.empty())
147152
saveSnapshot(pPlotState);
148153

149154
// restore the figure margins
@@ -162,7 +167,8 @@ void removeGraphicsDevice(boost::shared_ptr<PlotState> pPlotState)
162167

163168
void onBeforeNewPlot(boost::shared_ptr<PlotState> pPlotState)
164169
{
165-
if (pPlotState->hasPlots)
170+
if (pPlotState->hasPlots &&
171+
pPlotState->sizeBehavior == PlotSizeAutomatic)
166172
{
167173
saveSnapshot(pPlotState);
168174
}
@@ -188,7 +194,9 @@ void onConsolePrompt(boost::shared_ptr<PlotState> pPlotState,
188194
} // anonymous namespace
189195

190196
// begins capturing plot output
191-
core::Error beginPlotCapture(int pixelWidth, const FilePath& plotFolder)
197+
core::Error beginPlotCapture(double height, double width,
198+
PlotSizeBehavior sizeBehavior,
199+
const FilePath& plotFolder)
192200
{
193201
// clean up any stale plots from the folder
194202
std::vector<FilePath> folderContents;
@@ -210,11 +218,15 @@ core::Error beginPlotCapture(int pixelWidth, const FilePath& plotFolder)
210218
}
211219
}
212220

213-
// marker for content; this is necessary because on Windows, turning off
214-
// the png device writes an empty PNG file even if nothing was plotted, and
215-
// we need to avoid treating that file as though it were an actual plot
221+
// infer height/width if only one is given
222+
if (height == 0 && width > 0)
223+
height = width / kGoldenRatio;
224+
else if (height > 0 && width == 0)
225+
width = height * kGoldenRatio;
226+
227+
// create state accumulator
216228
boost::shared_ptr<PlotState> pPlotState = boost::make_shared<PlotState>(
217-
plotFolder);
229+
plotFolder, sizeBehavior);
218230

219231
// save old figure parameters
220232
r::exec::RFunction par("par");
@@ -229,26 +241,44 @@ core::Error beginPlotCapture(int pixelWidth, const FilePath& plotFolder)
229241
pPlotState->sexpMargins.set(sexpMargins);
230242

231243
// create the notebook graphics device
232-
error = r::exec::RFunction(".rs.createNotebookGraphicsDevice",
233-
plotFolder.absolutePath() + "/" kPlotPrefix "%03d.png",
234-
pixelWidth,
235-
r::session::graphics::device::devicePixelRatio(),
236-
r::session::graphics::extraBitmapParams()).call();
244+
r::exec::RFunction createDevice(".rs.createNotebookGraphicsDevice");
245+
246+
// the folder in which to place the rendered plots (this is a sibling of the
247+
// main chunk output folder)
248+
createDevice.addParam(
249+
plotFolder.absolutePath() + "/" kPlotPrefix "%03d.png");
250+
251+
// device dimensions
252+
createDevice.addParam(height);
253+
createDevice.addParam(width);
254+
255+
// sizing behavior drives units -- user specified units are in inches but
256+
// we use pixels when scaling automatically
257+
createDevice.addParam(sizeBehavior == PlotSizeManual ? "in" : "px");
258+
259+
// devie parameters
260+
createDevice.addParam(r::session::graphics::device::devicePixelRatio());
261+
createDevice.addParam(r::session::graphics::extraBitmapParams());
262+
error = createDevice.call();
237263
if (error)
238264
{
239265
// this is fatal; without a graphics device we can't capture plots
240266
return error;
241267
}
242268

243-
// turn on display list recording so we can do intelligent resizing later
244-
r::exec::RFunction devControl("dev.control");
245-
devControl.addParam("displaylist", "enable");
246-
error = devControl.call();
247-
if (error)
269+
// if sizing automatically, turn on display list recording so we can do
270+
// intelligent resizing later
271+
if (sizeBehavior == PlotSizeAutomatic)
248272
{
249-
// non-fatal since we'll do best-effort resizing in the absence of
250-
// display lists
251-
LOG_ERROR(error);
273+
r::exec::RFunction devControl("dev.control");
274+
devControl.addParam("displaylist", "enable");
275+
error = devControl.call();
276+
if (error)
277+
{
278+
// non-fatal since we'll do best-effort resizing in the absence of
279+
// display lists
280+
LOG_ERROR(error);
281+
}
252282
}
253283

254284
// set notebook-friendly figure margins

src/cpp/session/modules/rmarkdown/NotebookPlots.hpp

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,15 @@ namespace modules {
3434
namespace rmarkdown {
3535
namespace notebook {
3636

37-
core::Error beginPlotCapture(int pixelWidth, const core::FilePath& plotFolder);
37+
enum PlotSizeBehavior
38+
{
39+
PlotSizeAutomatic,
40+
PlotSizeManual
41+
};
42+
43+
core::Error beginPlotCapture(double height, double width,
44+
PlotSizeBehavior sizeBehavior,
45+
const core::FilePath& plotFolder);
3846

3947
core::Error initPlots();
4048

src/cpp/session/modules/rmarkdown/SessionRmdNotebook.cpp

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -209,8 +209,10 @@ Error setChunkConsole(const json::JsonRpcRequest& request,
209209
s_execContext->disconnect();
210210

211211
// create the execution context and connect it immediately if necessary
212-
s_execContext.reset(new ChunkExecContext(docId, chunkId, execScope, options,
213-
pixelWidth, charWidth));
212+
const json::Object& optionsJson = jsonOptions.type() == json::ObjectType ?
213+
jsonOptions.get_obj() : json::Object();
214+
s_execContext.reset(new ChunkExecContext(docId, chunkId, execScope,
215+
optionsJson, pixelWidth, charWidth));
214216
if (s_activeConsole == chunkId)
215217
s_execContext->connect();
216218

src/gwt/src/org/rstudio/studio/client/workbench/views/source/editors/text/ChunkOutputWidget.java

Lines changed: 37 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
import org.rstudio.core.client.ColorUtil;
1818
import org.rstudio.core.client.VirtualConsole;
1919
import org.rstudio.core.client.dom.DomUtils;
20+
import org.rstudio.core.client.dom.ImageElementEx;
2021
import org.rstudio.core.client.js.JsArrayEx;
2122
import org.rstudio.core.client.widget.FixedRatioWidget;
2223
import org.rstudio.core.client.widget.PreWidget;
@@ -659,12 +660,24 @@ private void showPlotOutput(String url, final boolean ensureVisible)
659660

660661
final Image plot = new Image();
661662

662-
final FixedRatioWidget fixedFrame = new FixedRatioWidget(plot,
663-
ChunkOutputUi.OUTPUT_ASPECT,
664-
ChunkOutputUi.MAX_PLOT_WIDTH);
665-
666-
root_.add(fixedFrame);
663+
if (isFixedSizePlotUrl(url))
664+
{
665+
// if the plot is of fixed size, emit it directly, but make it
666+
// initially invisible until we get sizing information (as we may
667+
// have to downsample)
668+
plot.setVisible(false);
669+
root_.add(plot);
670+
}
671+
else
672+
{
673+
// if we can scale the plot, scale it
674+
FixedRatioWidget fixedFrame = new FixedRatioWidget(plot,
675+
ChunkOutputUi.OUTPUT_ASPECT,
676+
ChunkOutputUi.MAX_PLOT_WIDTH);
667677

678+
root_.add(fixedFrame);
679+
}
680+
668681
DOM.sinkEvents(plot.getElement(), Event.ONLOAD);
669682
DOM.setEventListener(plot.getElement(), createPlotListener(plot,
670683
ensureVisible));
@@ -977,13 +990,32 @@ public void onBrowserEvent(Event event)
977990
{
978991
if (DOM.eventGetType(event) != Event.ONLOAD)
979992
return;
993+
994+
// if the image is of fixed size, just clamp its width while
995+
// preserving its aspect ratio
996+
if (isFixedSizePlotUrl(plot.getUrl()))
997+
{
998+
ImageElementEx img = plot.getElement().cast();
999+
img.getStyle().setProperty("height", "auto");
1000+
img.getStyle().setProperty("width", "100%");
1001+
img.getStyle().setProperty("maxWidth",
1002+
Math.min(ChunkOutputUi.MAX_PLOT_WIDTH,
1003+
img.naturalWidth()) + "px");
1004+
}
9801005

1006+
plot.setVisible(true);
1007+
9811008
renderTimeout.cancel();
9821009
completeUnitRender(ensureVisible);
9831010
}
9841011
};
9851012
}
9861013

1014+
private boolean isFixedSizePlotUrl(String url)
1015+
{
1016+
return url.contains("fixed_size=1");
1017+
}
1018+
9871019
@UiField Image clear_;
9881020
@UiField Image expand_;
9891021
@UiField HTMLPanel root_;

0 commit comments

Comments
 (0)