Skip to content

Add inline data explorer to R cells and Quarto documents#12693

Merged
jmcphers merged 16 commits intomainfrom
feature/quarto-inline-data-explorer
Apr 1, 2026
Merged

Add inline data explorer to R cells and Quarto documents#12693
jmcphers merged 16 commits intomainfrom
feature/quarto-inline-data-explorer

Conversation

@jmcphers
Copy link
Copy Markdown
Collaborator

@jmcphers jmcphers commented Mar 25, 2026

This change adds support for inline data explorers to R cells in Jupyter notebooks and for both R and Python in Quarto documents with inline output. It extends the work @nstrayer did to support Pandas data frames in Python notebooks.

image

Requires posit-dev/qa-example-content#117
Requires posit-dev/ark#1124
Addresses #12148

Release Notes

New Features

Bug Fixes

  • N/A

QA Notes

E2E tests for both Python and R are included.

This feature can be disabled using this (existing) setting:

image

Test tags: @:quarto

@jmcphers jmcphers requested a review from nstrayer March 25, 2026 16:33
@github-actions
Copy link
Copy Markdown

github-actions bot commented Mar 25, 2026

E2E Tests 🚀
This PR will run tests tagged with: @:critical @:quarto

readme  valid tags

nstrayer
nstrayer previously approved these changes Mar 26, 2026
Copy link
Copy Markdown
Contributor

@nstrayer nstrayer left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a few questions about duplication. Things are working and I really appreciate the scroll fixes!

I'm totally good to defer the de-duplication to a different PR. )Also perhaps it's all premature optimization at this point.)

Comment on lines +575 to +597
const newVerticalOffset = pinToRange(
context.instance.verticalScrollOffset + deltaY,
0,
context.instance.maximumVerticalScrollOffset
);

// If the scroll position wouldn't actually change, let the event
// bubble up so parent containers (e.g. Quarto documents) can scroll.
const horizontalChanged = newHorizontalOffset !== context.instance.horizontalScrollOffset;
const verticalChanged = newVerticalOffset !== context.instance.verticalScrollOffset;
if (!horizontalChanged && !verticalChanged) {
return;
}

// Consume the event to prevent scroll chaining to parent containers.
e.preventDefault();
e.stopPropagation();

// Record the last wheel event.
setLastWheelEvent(e.timeStamp);

// Set the scroll offsets.
context.instance.setScrollOffsets(newHorizontalOffset, newVerticalOffset);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh man. Thanks for fixing this.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, the scroll trap was too annoying until this was addressed!

.inline-data-explorer-container {
display: flex;
flex-direction: column;
/* If you change the border width, update BORDER in quartoInlineDataExplorerLayout.ts */
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably not in this PR but I wonder if we should do something like

export const INLINE_DE_LAYOUT = {
      toolbarHeight: 24,
      toolbarBorder: 1,
      containerBorder: 1,
      // ...
} as const;

// ...

const layoutVars = {
      '--inline-de-toolbar-height': `${INLINE_DE_LAYOUT.toolbarHeight}px`,
      '--inline-de-toolbar-border': `${INLINE_DE_LAYOUT.toolbarBorder}px`,
      '--inline-de-container-border': `${INLINE_DE_LAYOUT.containerBorder}px`,
} as React.CSSProperties;

In the future to make this harder to break. (Although maybe LLMs handle this totally fine with comments like this.)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's nice to have constants in only one place but then you have to move the CSS out of the CSS file! 😭What we really need is some sort of CSS preprocessor we can feed variables to (or --var values)...

Comment on lines +1796 to +1798
// Exception: when data explorer is present, always keep text/plain. The data
// explorer is a live component that won't work after the document is closed
// and reopened; text/plain serves as the cache-safe fallback for that case.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is interesting. In notebooks we just keep everything and then try and find the comm and fallback after we dont find it. I like this approach a lot though.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did the fallback thing originally, but it was hard to render nicely -- you'd see all the explorers TRYING to open and then failing.

Comment on lines +1830 to +1831
(!mime.startsWith('application/vnd.positron.') ||
mime === DATA_EXPLORER_MIME_TYPE)) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I really feel like we should eventually have a nice abstraction for dealing with mime type detection etc.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should!

const { commId, shape, title, variablePath, documentUri, onFallback, onHeightChange } = props;
const services = PositronReactServices.services;
const [state, setState] = useState<QuartoInlineDataExplorerState>({ status: 'loading' });
const containerRef = useRef<HTMLDivElement>(null);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is never used, right?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is not! Deleted.

const [maxHeight, setMaxHeight] = useState<number>(
services.configurationService.getValue<number>(
POSITRON_QUARTO_INLINE_DATA_EXPLORER_MAX_HEIGHT_KEY
) ?? 300
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pull 300 to constant as it's used elsewhere?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done!

Comment on lines +50 to +75
function InlineDataExplorerHeader({ title, shape, onOpenInExplorer }: {
title: string;
shape: { rows: number; columns: number };
onOpenInExplorer?: () => void;
}) {
return (
<div className='inline-data-explorer-header'>
<div className='inline-data-explorer-info'>
<span className='inline-data-explorer-title'>{title}</span>
<span className='inline-data-explorer-shape'>
{shape.rows.toLocaleString()} {localize('rows', 'rows')} x {shape.columns.toLocaleString()} {localize('columns', 'columns')}
</span>
</div>
{onOpenInExplorer && (
<button
className='inline-data-explorer-open-button'
title={localize('openInDataExplorer', 'Open in Data Explorer')}
onClick={onOpenInExplorer}
>
<span className='codicon codicon-go-to-file' />
{localize('openInDataExplorer', 'Open in Data Explorer')}
</button>
)}
</div>
);
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we just export InlineDataExplorerHeader from the notebook version? The function is identical.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sure, done!

Comment on lines +82 to +83
export function QuartoInlineDataExplorer(props: QuartoInlineDataExplorerProps) {
const { commId, shape, title, variablePath, documentUri, onFallback, onHeightChange } = props;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Curious about your opinion about extracting the shared logic to a common InlineDataExplorer component with an interface something like:

export interface InlineDataExplorerProps {
        commId: string;
        shape: { rows: number; columns: number };
        title: string;
        variablePath?: string[];
        documentUri: URI;
        onFallback?: () => void;
        onHeightChange?: (height: number) => void;
        className?: string;       // For Quarto's font override class
}

Then quarto and notebooks have simple components around it with some hooks to fill in the differences.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A good idea to follow up on! I didn't want to run the risk of refactoring the existing notebook code as I'm not as familiar with it. Maybe worth doing next time we're in there.

/**
* Configuration key for the maximum height of the inline data explorer.
*/
export const POSITRON_QUARTO_INLINE_DATA_EXPLORER_MAX_HEIGHT_KEY = 'positron.quarto.inlineDataExplorer.maxHeight';
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

would it be reasonable to share this between quarto and notebooks?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After some research I think we should just use the existing notebook setting for this! I've removed the Quarto specific one.

@jmcphers jmcphers merged commit 6a532dd into main Apr 1, 2026
36 of 37 checks passed
@jmcphers jmcphers deleted the feature/quarto-inline-data-explorer branch April 1, 2026 04:05
@github-actions github-actions bot locked and limited conversation to collaborators Apr 1, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants