Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions examples/compiled/bar_size_responsive.vg.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@
{
"update": "isFinite(containerSize()[0]) ? containerSize()[0] : 300",
"events": "window:resize"
},
{
"update": "isFinite(containerSize()[0]) ? containerSize()[0] : 300",
"events": "container:resize"
}
]
}
Expand Down
2 changes: 1 addition & 1 deletion site/docs/view/size.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ After setting `width` or `height` to `"container"`, you need to ensure that the
**Limitations:**

- This responsive mode is available only for single view or layer specifications.
- Vega listens to the `window.resize` event to update plot size from container size. This should cover many use cases. However, if you change the container size programmatically (e.g., you build a custom divider view), you'll need to trigger `window.resize` manually. In a modern browser, you can do: `window.dispatchEvent(new Event('resize'));`.
- Vega updates the plot size whenever the container changes size, for whatever reason: a window resize, a CSS layout change, or a script resizing the element. This requires Vega 6.4 or later and a browser with [`ResizeObserver`](https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserver) support.

### Specifying Width and Height per Discrete Step

Expand Down
14 changes: 13 additions & 1 deletion src/compile/layoutsize/assemble.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,19 @@ export function sizeSignals(model: Model, sizeType: LayoutSizeType): (NewSignal
const expr = isWidth ? 'containerSize()[0]' : 'containerSize()[1]';
const defaultValue = getViewConfigContinuousSize(model.config.view, isWidth ? 'width' : 'height');
const safeExpr = `isFinite(${expr}) ? ${expr} : ${defaultValue}`;
return [{name, init: safeExpr, on: [{update: safeExpr, events: 'window:resize'}]}];
return [
{
name,
init: safeExpr,
// `container:resize` catches every container size change but only exists in newer Vega;
// `window:resize` keeps older Vega working and costs nothing when both fire, since the
// signal then re-reads the same size and Vega skips unchanged signal updates.
on: [
{update: safeExpr, events: 'window:resize'},
{update: safeExpr, events: 'container:resize'},
],
},
];
} else {
return [
{
Expand Down
39 changes: 39 additions & 0 deletions test-runtime/resize.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import {parse, resetSVGDefIds, View} from 'vega';
import {describe, expect, it} from 'vitest';
import {compile, TopLevelSpec} from '../src/index.js';

describe('Responsive container sizing at runtime', () => {
it('updates width and height when the container resizes', async () => {
document.body.innerHTML = '';
resetSVGDefIds();

const div = document.createElement('div');
div.style.width = '240px';
div.style.height = '160px';
document.body.appendChild(div);

const spec: TopLevelSpec = {
width: 'container',
height: 'container',
padding: 0,
data: {values: [{a: 0}]},
mark: 'point',
};

const view = new View(parse(compile(spec).spec), {container: div, renderer: 'svg'});
await view.runAsync();

expect(view.signal('width')).toBe(240);
expect(view.signal('height')).toBe(160);

div.style.width = '360px';
div.style.height = '220px';

// picked up via the compiled spec's container:resize handler, which needs a Vega
// release that has the container event source (vega/vega#4318)
await expect.poll(() => view.signal('width')).toBe(360);
await expect.poll(() => view.signal('height')).toBe(220);

view.finalize();
});
});
20 changes: 16 additions & 4 deletions test/compile/compile.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -371,7 +371,10 @@ describe('compile/compile', () => {
{
name: 'width',
init: 'isFinite(containerSize()[0]) ? containerSize()[0] : 300',
on: [{events: 'window:resize', update: 'isFinite(containerSize()[0]) ? containerSize()[0] : 300'}],
on: [
{events: 'window:resize', update: 'isFinite(containerSize()[0]) ? containerSize()[0] : 300'},
{events: 'container:resize', update: 'isFinite(containerSize()[0]) ? containerSize()[0] : 300'},
],
},
]);
expect(spec.width).toBeUndefined();
Expand Down Expand Up @@ -447,7 +450,10 @@ describe('compile/compile', () => {
{
name: 'height',
init: 'isFinite(containerSize()[1]) ? containerSize()[1] : 300',
on: [{events: 'window:resize', update: 'isFinite(containerSize()[1]) ? containerSize()[1] : 300'}],
on: [
{events: 'window:resize', update: 'isFinite(containerSize()[1]) ? containerSize()[1] : 300'},
{events: 'container:resize', update: 'isFinite(containerSize()[1]) ? containerSize()[1] : 300'},
],
},
]);
expect(spec.height).toBeUndefined();
Expand All @@ -473,12 +479,18 @@ describe('compile/compile', () => {
{
name: 'width',
init: 'isFinite(containerSize()[0]) ? containerSize()[0] : 500',
on: [{events: 'window:resize', update: 'isFinite(containerSize()[0]) ? containerSize()[0] : 500'}],
on: [
{events: 'window:resize', update: 'isFinite(containerSize()[0]) ? containerSize()[0] : 500'},
{events: 'container:resize', update: 'isFinite(containerSize()[0]) ? containerSize()[0] : 500'},
],
},
{
name: 'height',
init: 'isFinite(containerSize()[1]) ? containerSize()[1] : 300',
on: [{events: 'window:resize', update: 'isFinite(containerSize()[1]) ? containerSize()[1] : 300'}],
on: [
{events: 'window:resize', update: 'isFinite(containerSize()[1]) ? containerSize()[1] : 300'},
{events: 'container:resize', update: 'isFinite(containerSize()[1]) ? containerSize()[1] : 300'},
],
},
]);
expect(spec.width).toBeUndefined();
Expand Down
34 changes: 34 additions & 0 deletions test/compile/layoutsize/assemble.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -169,5 +169,39 @@ describe('compile/layout', () => {
const height = sizeSignals(model, 'height');
expect(height).toEqual([{name: 'height', value: 18}]);
});

it('should update container size on window and container resize', () => {
const model = parseUnitModelWithScaleAndLayoutSize({
width: 'container',
height: 'container',
mark: 'point',
encoding: {},
config: {view: {continuousWidth: 500, continuousHeight: 400}},
});

const width = sizeSignals(model, 'width');
expect(width).toEqual([
{
name: 'width',
init: 'isFinite(containerSize()[0]) ? containerSize()[0] : 500',
on: [
{events: 'window:resize', update: 'isFinite(containerSize()[0]) ? containerSize()[0] : 500'},
{events: 'container:resize', update: 'isFinite(containerSize()[0]) ? containerSize()[0] : 500'},
],
},
]);

const height = sizeSignals(model, 'height');
expect(height).toEqual([
{
name: 'height',
init: 'isFinite(containerSize()[1]) ? containerSize()[1] : 400',
on: [
{events: 'window:resize', update: 'isFinite(containerSize()[1]) ? containerSize()[1] : 400'},
{events: 'container:resize', update: 'isFinite(containerSize()[1]) ? containerSize()[1] : 400'},
],
},
]);
});
});
});