Skip to content

Commit

Permalink
Improve zoom scaling based on device pixels.
Browse files Browse the repository at this point in the history
- Adjusts the output scaling logic to account for device pixel ratios other
  than one, which can occur on high-density/retina displays or when the
  browser window is zoomed.
- Modifies slider to accept non-integer-pixel zoom levels.
- Changes figure interpolation to use smooth interpolation for zoom levels that
  are not integer multiples of the default size, to avoid visual artifacts.
- Allows configuring the default number of pixels per cell used by the ArrayAutovisualizer.

PiperOrigin-RevId: 668073705
  • Loading branch information
danieldjohnson authored and Treescope Developers committed Aug 27, 2024
1 parent 0f9923b commit 5e429cb
Show file tree
Hide file tree
Showing 4 changed files with 51 additions and 18 deletions.
7 changes: 7 additions & 0 deletions treescope/_internal/api/array_autovisualizer.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,11 @@ class ArrayAutovisualizer:
visualization. Useful for seeing small array values.
token_lookup_fn: Optional function that looks up token IDs and adds them to
the visualization on hover.
pixels_per_cell: Size of each rendered array element in pixels, between 1
and 21 inclusive. This controls the zoom level of the rendering. Array
elements are always drawn at 7 pixels per cell and then rescaled, so
out-of-bounds annotations and "digitbox" integer value patterns may not
display correctly at fewer than 7 pixels per cell.
"""

maximum_size: int = 4_000
Expand All @@ -81,6 +86,7 @@ class ArrayAutovisualizer:
force_continuous: bool = False
include_repr_line_threshold: int = 5
token_lookup_fn: Callable[[int], str] | None = None
pixels_per_cell: int | float = 7

def _autovisualize_array(
self,
Expand Down Expand Up @@ -159,6 +165,7 @@ def _autovisualize_array(
axis_item_labels=None,
value_item_labels=value_item_labels,
axis_labels=None,
pixels_per_cell=self.pixels_per_cell,
)
outputs = [array_rendering]
last_line_parts = []
Expand Down
4 changes: 2 additions & 2 deletions treescope/_internal/api/arrayviz.py
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ def render_array(
axis_item_labels: dict[AxisName | int, list[str]] | None = None,
value_item_labels: dict[int, str] | None = None,
axis_labels: dict[AxisName | int, str] | None = None,
pixels_per_cell: int = 7,
pixels_per_cell: int | float = 7,
) -> figures_impl.TreescopeFigure:
"""Renders an array (positional or named) to a displayable HTML object.
Expand Down Expand Up @@ -386,7 +386,7 @@ def _render_pretruncated(
axis_item_labels: dict[AxisName | int, list[str]] | None,
value_item_labels: dict[int, str] | None,
axis_labels: dict[AxisName | int, str] | None,
pixels_per_cell: int = 7,
pixels_per_cell: int | float = 7,
) -> arrayviz_impl.ArrayvizRendering:
"""Internal helper to render an array that has already been truncated."""
if axis_item_labels is None:
Expand Down
2 changes: 1 addition & 1 deletion treescope/_internal/arrayviz_impl.py
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ def render_array_data_to_html(
dynamic_continous_cmap: bool = False,
raw_min_abs: float | None = None,
raw_max_abs: float | None = None,
pixels_per_cell: int = 7
pixels_per_cell: int | float = 7
) -> str:
"""Helper to render an array to HTML by passing arguments to javascript.
Expand Down
56 changes: 41 additions & 15 deletions treescope/_internal/js/arrayviz.js
Original file line number Diff line number Diff line change
Expand Up @@ -896,11 +896,9 @@ const arrayviz = (() => {

container.style.width = 'fit-content';
container.style.height = 'fit-content';
container.style.setProperty('image-rendering', 'pixelated');
container.style.fontFamily = 'monospace';
container.style.transformOrigin = 'top left';
container.style.setProperty('image-rendering', 'pixelated');

container.style.setProperty('--arrayviz-zoom', '1');
container.style.setProperty('--base-font-size', '14px');

Expand Down Expand Up @@ -1013,19 +1011,19 @@ const arrayviz = (() => {
const datalist =
/** @type {!HTMLDataListElement} */ (
destination.appendChild(document.createElement('datalist')));
datalist.id = "arrayviz-markers";
for (const val of [1, 7, 14, 21]) {
datalist.id = 'arrayviz-markers';
for (const val of [1, 2, 3.5, 7, 14, 21]) {
const datalistOption = /** @type {!HTMLOptionElement} */ (
datalist.appendChild(document.createElement('option')));
datalistOption.value = val;
}
const zoomslider =
/** @type {!HTMLInputElement} */ (document.createElement('input'));
zoomslider.type = 'range';
zoomslider.value = pixelsPerCell;
zoomslider.min = '1';
zoomslider.max = '21';
zoomslider.step = '1';
zoomslider.step = 'any';
zoomslider.value = pixelsPerCell;
zoomslider.setAttribute('list', datalist.id);
const infodiv = /** @type {!HTMLDivElement} */ (
destination.appendChild(document.createElement('div')));
Expand Down Expand Up @@ -1089,18 +1087,48 @@ const arrayviz = (() => {

// Event handlers.
let pendingZoomPixels = 0;
const updateZoomFromSlider = () => {
// For accurate pixel alignment: Round device pixel ratio to an integer
// so that zoom level 1 is an integer number of pixels.
let roundedPxRatio = Math.round(window.devicePixelRatio);
if (window.devicePixelRatio < 1) {
roundedPxRatio = 1 / Math.round(1 / window.devicePixelRatio);
}
const pixelRatioAdjustment = roundedPxRatio / window.devicePixelRatio;
// For accurate pixel alignment: Round to the closest number of physical
// pixels, then map back to logical scale.
let cssPxTarget = parseFloat(zoomslider.value);
if (cssPxTarget < 1) {
cssPxTarget = 1;
}
if ((cssPxTarget * roundedPxRatio) % cellSize == 0) {
container.style.setProperty('image-rendering', 'pixelated');
} else {
container.style.setProperty('image-rendering', 'auto');
}
const scale = (cssPxTarget / cellSize) * pixelRatioAdjustment;
container.style.setProperty('--arrayviz-zoom', `${scale}`);
fixMargins();
};
const watchPixelRatio = () => {
const media =
window.matchMedia(`(resolution: ${window.devicePixelRatio}dppx)`);
const listener = () => {
media.removeEventListener('change', listener);
updateZoomFromSlider();
watchPixelRatio();
};
media.addEventListener('change', listener);
};
zoomslider.addEventListener('input', () => {
pendingZoomPixels = 0;
const oldTop = zoomslider.offsetTop;
const scale = parseFloat(zoomslider.value) / cellSize;
container.style.setProperty('--arrayviz-zoom', `${scale}`);
fixMargins();
updateZoomFromSlider();
// Try to keep it in a consistent place:
window.scrollBy(
{top: zoomslider.offsetTop - oldTop, behavior: 'instant'});
// In case that didn't work, make sure it's at least visible:
zoomslider.scrollIntoView({behavior: 'instant', block: 'nearest'});
console.log('Fixing scroll');
});
canvas.addEventListener('mousemove', (evt) => {
const lookup = lookupPixel(evt.offsetX, evt.offsetY);
Expand Down Expand Up @@ -1189,9 +1217,7 @@ const arrayviz = (() => {
}
}
zoomslider.value = curZoomLevel;
const scale = curZoomLevel / cellSize;
container.style.setProperty('--arrayviz-zoom', `${scale}`);
fixMargins();
updateZoomFromSlider();
// Try to keep it in a consistent place. offsetX/offsetY are relative to
// the zoom level, so we have to scale them.
const baseRect = inner.getBoundingClientRect();
Expand All @@ -1207,8 +1233,8 @@ const arrayviz = (() => {

// Draw the initial output.
drawAllCells();
fixMargins();
zoomslider.dispatchEvent(new Event('input'));
updateZoomFromSlider();
watchPixelRatio();
}

/**
Expand Down

0 comments on commit 5e429cb

Please sign in to comment.