Skip to content

Fixed layouts and regions

Some data has places of its own: countries on a map, rooms on a floor plan, panels on a board. A fixed scene draws each item at the rectangle you give it, at the size you give it, and a region fills that rectangle, or the outline you give it, edge to edge. Zoom into an island below: its districts appear exactly where it was drawn, and the other island stays where it was. Point at the lake on South Isle: it is not part of the island.

Running the code below: fixed-layout.tsx
fixed-layout.tsx
import { createSceneSource, DataExplorer, type ExplorerNode, type ExplorerRect } from "@data-xplorer/core";
// Two made-up islands, each split into districts that share their borders exactly. Every shape is
// in one set of map units, as projected map data would be; nothing here is laid out for you.
type Ring = number[][];
interface Place {
name: string;
outline: Ring[]; // its edge, then any holes
districts?: Place[];
}
const islands: Place[] = [
{
name: "North Isle",
outline: [[[30, 40], [90, 20], [160, 45], [150, 100], [60, 110], [20, 80]]],
districts: [
{ name: "Westfold", outline: [[[30, 40], [90, 20], [100, 105.6], [60, 110], [20, 80]]] },
{ name: "Eastmarch", outline: [[[90, 20], [160, 45], [150, 100], [100, 105.6]]] },
],
},
{
name: "South Isle",
outline: [
[[160, 130], [230, 110], [280, 150], [250, 190], [170, 180], [150, 155]],
[[230, 150], [245, 150], [245, 162], [230, 162]], // a lake: not part of the island
],
districts: [
{ name: "Lowmoor", outline: [[[160, 130], [205, 117.1], [205, 184.4], [170, 180], [150, 155]]] },
{
name: "Lakeside",
outline: [
[[205, 117.1], [230, 110], [280, 150], [250, 190], [205, 184.4]],
[[230, 150], [245, 150], [245, 162], [230, 162]],
],
},
],
},
];
// The smallest rectangle round a place's edge: where it sits, and the bounds of its own scene.
function boxOf(place: Place): ExplorerRect {
const xs = place.outline[0]!.map(([x]) => x!);
const ys = place.outline[0]!.map(([, y]) => y!);
const x = Math.min(...xs);
const y = Math.min(...ys);
return { x, y, width: Math.max(...xs) - x, height: Math.max(...ys) - y };
}
const region = (place: Place, i: number): ExplorerNode<string> => ({
id: place.name,
label: place.name,
weight: 1, // ignored: a fixed scene draws each item at its own box
href: `#${place.name}`,
drillsInto: place.districts ? place.name : null,
shape: "region",
box: boxOf(place),
outline: [place.outline], // one polygon: its edge, then its holes
ramp: i / 3,
});
const source = createSceneSource<string>({
load(focus) {
const island = islands.find((i) => i.name === focus);
const places = island ? island.districts! : islands;
return {
id: focus ?? "map",
heading: focus ?? "The islands",
layout: "fixed",
// An island's scene is bounded by that island's own box, so its districts land exactly on it.
bounds: island ? boxOf(island) : { x: 0, y: 0, width: 300, height: 200 },
clusters: island ? [] : places.map(region),
nodes: island ? places.map(region) : [],
edges: [],
};
},
parentOf: (focus) => ({ kind: "anchored", focus: null, anchorId: focus }),
});
export default function FixedLayout() {
return <DataExplorer source={source} focus={null} />;
}
  • layout: "fixed" on a scene, and a box ({ x, y, width, height }) on each item, in units of your choosing. An item without a box is not drawn. weight is not used for size.
  • bounds is the scene’s extent in the same units; without it, the smallest rectangle round its items. A fixed scene streamed in pages should set it, or the extent grows with each page and everything already drawn moves.
  • Children land on their parent when a fixed scene is entered from an item of a fixed scene: the child scene’s bounds is fitted exactly onto the item’s box. So keep one set of units all the way down, and give each scene its parent item’s box as bounds.
  • shape: "region" fills its box. With an outline, it is that shape instead: a list of polygons, each a list of rings (its edge, then its holes), each a list of [x, y] points, in the same units as the box. That is a GeoJSON MultiPolygon’s coordinates once projected, so a Polygon’s coordinates go in as [coordinates]. Keep the outline inside the box.
  • Hit-testing follows the outline: a hole is not part of the region, and nor is the box around it. Hover, selection and the ring emphasis are drawn along the outline.
  • Labels go inside a region, in the middle of its box, or at labelAt (a point in the same units) when the middle of the box falls outside the shape.
  • The other shapes work in a fixed scene too: a square fills its box, and a disc or cloud is the largest circle that fits it. A custom renderer gets the box as frame.width and frame.height.
  • Your units do not matter. Each fixed scene is scaled so its longer side is 1,000 units of its own world, the size of any other scene’s, so a small country deep in a map zooms in like anything else.
  • The accessible outline lists a fixed scene’s items in the order you give them, clusters first: for a map, alphabetical is usually right.