tilez is a generic layout engine for Svelte components.
Above layout was generated by tilez, where root tile props (width, height and padding) are changing over time. Boxes shown are a mixture of HTML, SVG, Canvas and WebGL elements.
By default, all tiles – the building blocks of a layout – are renderless components. A layout is defined via nested, stackable tiles, where each tile has its own coordinate space, which is accessible through Svelte stores within a tile's context.
tilez is
The main application of tilez is to use it as abstraction layer for creating compositions and layers of arbitrary SVG charts for data visualization. As SVG is lacking a layout engine, tilez fills that gap. However, it can be used with HTML, Canvas and WebGL, too.
Here is an example of a composition of several different Observable Plot charts, which makes up an UpSet plot. Individual charts are embedded in a simple tilez layout. The final result is one single SVG chart (w/o HTML).
tilez is also helpful for building dashboards. It can easily create open, table, stratified and grouped layouts (see also Dashboard Design Patterns). With tilez, pure SVG dashboards can be implemented effortlessly.
Above dashboard in open layout contains charts using Apache ECharts library.
Look at accompanying packages
for using chart libraries with tilez.
Install tilez as npm package via
npm install tilez
A Tile component is a building block of a layout.
A Tile component can be imported by
import { Tile } from 'tilez';
A Tile component has following available props (see API Tile Props for details)
<Tile
stack="horizontal"
width="800px"
height="600px"
innerPadding="10px"
outerPadding="5px"
hAlign="left"
vAlign="top"
type="plain"
mode="spacing"
>
...
</Tile>
All props are optional, i.e. you can have tiles with no props at all
<Tile>
...
</Tile>
Tile props are reactive (except for stack and type), i.e. if you change props for an arbitrary tile in tile hierarchy, whole subtree of tiles is rerendered according to updated props.
If you want to know exactly what happens, when you alter these props in a given layout – without reading through API Tile Props – take a look at tilez.spren9er.de and play around with an interactive example layout.
The main concept of tilez is stacking tiles in horizontal or vertical direction, recursively. Within a stack, a tile starts at the point where the last tile ends. For stacking, you use the property stack, which defines in which direction children tiles should be stacked.
For convenience, there are following shortcuts available
These components have the same props available as a basic Tile component (except for stack property).
Assuming no stack is given for a tile, then all children tiles will be layered. They share the same coordinate space like their parent tile. Also, they are rendered in the specified order. Thus, first tile will be displayed in the back, last tile in the front.
Layouts can be described in a declarative way, by defining props of nested tiles. Here is an example of a simple layout
<HTile width="400px" height="300px" innerPadding="10px" outerPadding="5px">
<Tile width="180px">
<MyComponent1 />
</Tile>
<Tile height="60%" vAlign="center">
<MyComponent2 />
</Tile>
<Tile width="30%">
<MyComponent3 />
</Tile>
</HTile>
Check out a similar example in Svelte REPL!
After defining a layout, arbitrary Svelte components can be embedded in your tiles. In your components you get access to tile specs, linear scales of local coordinate system and a reference to HTML/SVG/Canvas/WebGL element by adding the following lines
import { getTileContext } from 'tilez';
const {specs, xScale, yScale, element } = getTileContext();
All objects – which you obtain from a tile's context – are Svelte stores. See API Tile Context for details.
Alternatively, you can use getContext
from Svelte
import { getContext } from 'svelte';
const {specs, xScale, yScale, element } = getContext('tilez');
For Canvas or WebGL elements you can retrieve additionally CanvasRenderingContext2D or WebGLRenderingContext, respectively as Svelte store context
from getTileContext
.
Tile specs give you information about width and height of tile, as well as relative positions w.r.t. root, subroot and parent tile.
For further specs information see API Tile Specs.
For each tile, there are two linear scales $xScale
and $yScale
available, one for x-axis and one for y-axis.
Their domain is [0, 1]
and their range is [0, $specs.width]
or [0, $specs.height]
, respectively.
You can modify the domain for each scale.
import { getTileContext } from 'tilez';
const { xScale, yScale } = getTileContext();
const x = $xScale.domain([-5, 5]);
const y = $yScale.domain([0, 400]);
const sampleCoords = [x(0.5), y(150)];
Note: If you need non-linear scales, consider using d3-scale with given tile specs.
See also API Linear Scale.
There are following ways to get a reference to the underlying HTML, SVG, Canvas or WebGL element of a tile.
Within your component – which is embedded in a Tile component – you can get access to an element store by using getTileContext
.
import { getTileContext } from 'tilez';
const { element } = getTileContext();
$effect(() => {
doSomethingWith($element);
});
Elements are also accessible from outside of tile scope with following binding
<script lang="ts">
import { Tile } from 'tilez';
let element: SVGElement;
$effect(() => {
doSomethingWith($element);
});
</script>
<Tile type="svg" bind:element>
...
<Tile>
For Canvas and WebGL elements, you retrieve context for drawing from getTileContext. Here is an example of using Canvas in your component
const { specs, context } = getTileContext();
$effect.pre(() => {
if ($context) {
const ctx = $context as CanvasRenderingContext2D;
const dpr = window.devicePixelRatio || 1;
const thickness = 1 * dpr;
const offset = thickness / 2;
const width = $specs.width * dpr - thickness;
const height = $specs.height * dpr - thickness;
ctx.beginPath();
ctx.strokeStyle = '#cccccc';
ctx.lineWidth = thickness;
ctx.rect(offset, offset, width, height);
ctx.stroke();
ctx.closePath();
}
});
Note: When using Canvas tiles, make sure that you multiply specs coordinates with window.devicePixelRatio
.
The underlying layout algorithm should behave well in all circumstances, especially when there is not enough space to render all given tiles.
But which tiles should be rendered and which should be ignored?
In the following, we take a closer look at an opinionated rendering algorithm, which is implemented in tilez. We consider the algorithm for a single tile with a non-trivial stack direction ('horizontal' or 'vertical') and its direct children tiles. This algorithm then can be applied to each stack of the tiles hierarchy.
Before tiles are rendered within a stack, they are sorted according to following order
Within each group, tiles are sorted according to the natural order given (ascending order of children tiles within parent tile). The order gives information about the priority for rendering. First tiles of above order have high priority and will be rendered first, while last tiles have low priority and won't be rendered at all, if there is not enough space left.
Note: Alignment props are not taken into consideration when sorting! Thus, tile B which comes after tile A in natural order and belongs to same group, could be aligned as such that it appears before tile A. This fact can be used to take influence in the rendering behavior.
So far, we only computed the resulting size for each tile. Now, we consider the rendering algorithm. When all sizes are determined with the process above, tiles are grouped according to their alignment w.r.t. stack direction ('hAlign' for 'horizontal' and 'vAlign' for 'vertical'). This will generate three groups. We process them in the following way
In each step above, zero-sized tiles are ignored.
# tilez.Tile.stack · ('horizontal' | 'vertical') [default: undefined
]
When this property is not given, all children tiles will have the same coordinate space like current tile and they are layered in the natural order given. Otherwise, children tiles will be distributed within current tile according to their props in 'horizontal' or 'vertical' direction.
# tilez.Tile.width
Argument can be an absolute or relative number. Accepts strings like "500px", "500", "50%", "0.5" or numbers like 500 or 0.5. Numbers between 0 and 1 are interpreted as percentages, otherwise they represent absolute widths. The given width will result in different tile widths, depending on the layout mode. Relative widths refer to the width you obtain when you subtract all absolute tile widths from full width. When there is no width given (default), remaining width in parent tile – after rendering tiles with absolute and relative width – will be distributed equally between current tile and other tiles having no width specification. For root tile relative widths are not allowed. When no width is given in root tile, parent container needs to have explicit width specified, otherwise nothing is rendered.
# tilez.Tile.height
Analog to width above.
# tilez.Tile.innerPadding · [default: 0] [inherits]
Defines the padding between children tiles of current tile. Format must be either a string like "10px", "10" or a number like 10. Relative values are not supported. For layout mode 'spacing' it adds half of the given inner padding to the left and right of the outer tiles (or tile if there is only one). This property will be inherited, thus all children tiles will have the same inner padding for their children unless not specified explicitly in children tile. In other words, if inner padding of children tile is given, this value will be considered instead of inner padding of parent tile.
# tilez.Tile.outerPadding · [default: 0]
Defines the padding around children tile(s) of current tile. It is similar to CSS padding of a HTML container. This property won't be inherited.
# tilez.Tile.hAlign · ('left' | 'center' | 'right') [default: 'left']
Defines the horizontal alignment w.r.t. parent tile. Accepts 'left', 'center' and 'right'. When several children tiles share the same alignment property, they will be positioned as a group according to their given order within parent tile. For 'center' applies: If centered group can't be positioned in the center because there will be an overlap with 'left' or 'right' groups, it will be shifted to the right or left, respectively.
# tilez.Tile.vAlign · ('top' | 'center' | 'bottom') [default: 'top']
Defines the vertical alignment w.r.t. parent tile. Accepts 'top', 'center' and 'bottom'. It behaves like hAlign, but in vertical direction.
# tilez.Tile.type · ('plain' | 'svg' | 'html' | 'canvas' | 'webgl') [default: 'plain'] [inherits]
This property sets the document type of current tile. Available types are 'plain', 'svg', 'html', 'canvas' and 'webgl'. Type inherits from parent tile unless not specified explicitly. Otherwise, given type will be taken into consideration.
By default, using tilez won't create any containers, i.e. all components are renderless components (type is 'plain').
Using type 'svg', (sub)root tile will be an SVG container <svg>
and all children tiles will be rendered as SVG group <g>
.
For 'html' tiles, all <div>
containers are implicitly absolute positioned. If you want to have a pure HTML layout, you should probably consider using CSS flexbox and CSS grid, as they are more powerful and flexible.
If you use 'canvas' tiles, (sub)root tile creates a <canvas>
container. Within this container coordinate system is translated to each tile's origin. All children tiles will share the same <canvas>
element.
If you use 'webgl' tiles, (sub)root tile creates a <canvas>
container. All children tiles will share the same <canvas>
element and also the same coordinate system, even they are placed in a stack.
You can mix tile types, e.g. start with an HTML tile and add various subroot SVG, Canvas and/or WebGL tiles. Plain tiles can be added at each level in the tile hierarchy. However, there are following restrictions:
# tilez.Tile.mode · ('spacing' | 'sizing' ) [default: 'spacing'] [inherits]
There are two layout modes available: one which is optimized for 'spacing' and one for 'sizing'. They differ on how to interpret sizes when you specify a non-zero inner padding. When no inner padding is given, both modes produce the same layout.
In layout mode 'spacing' (default mode), inner padding is part of the size specification of a tile. For a given width of 100px and an inner padding of 20px, the resulting width of the tile is 80px. On each side of a tile, there is an empty space of 50% of inner padding, in our example 10px. The consequence is that in this layout spacings are aligned properly across stacks, e.g. the first gap of a tile with 50% width is aligned with the fifth gap of five stacked tiles with 10% width each.
Note: There will be empty space of 50% inner padding on all sides of the most outer tile. If you want equidistant paddings throughout the whole tile hierarchy, you can add an outer padding in root tile of inner padding / 2.
When you use absolute sizes together with relative sizes to define your layout, make sure to add outer padding to your size beforehand. As an example: You want to work with a width of 200px and outer padding is given as 10px. Then a tile of width 100px corresponds to a tile of 50% only when the initial width is defined as 220px, not 200px.
When layout mode 'sizing' is used, all tiles have exactly the size which is specified in tile props, i.e. for a given width of 100px, the tile has exactly a width of 100px (when there is enough space to render). Also, a tile of width 50% has 5 times the width of a tile of width 10% (if they are in the same stack!), which generally does not apply to 'spacing' layout mode.
It depends on your use case, which mode you choose. You can also mix modes, start with one mode and change to the other mode in an inner tile.
# tilez.Tile.element
A reference to an HTML, SVG or Canvas element (depending on the type). For renderless components ('plain' type), element is undefined
. See also this section.
# tilez.Tile.wrapper
A reference to the wrapper element (HTMLDivElement
) containing all tiles. This can be used to add styles via
<script lang="ts">
import { onMount } from 'svelte';
import { Tile } from 'tilez';
let wrapper: HTMLDivElement;
onMount(() => {
if (wrapper) wrapper.style.border = '2px solid red';
});
</script>
<Tile bind:wrapper>
...
<Tile>
Only applicable for root tile.
# tilez.getTileContext()
Returns an object containing following Svelte stores
# tilez.TileSpecs.width
Width of tile
# tilez.TileSpecs.height
Height of tile
# tilez.TileSpecs.rootX
The x-coordinate w.r.t. root tile
# tilez.TileSpecs.rootY
The y-coordinate w.r.t. root tile
# tilez.TileSpecs.subRootX
The x-coordinate w.r.t. subroot tile. When type of tile changes when going from parent to child tile, there will be a new subtree in tiles hierarchy. The root tile of this subtree is called a subroot tile. The x value is the distance between the left position of subroot tile and the left position of given tile in horizontal direction.
# tilez.TileSpecs.subRootY
The y-coordinate w.r.t. subroot tile. See subRootX for definition of a subroot.
# tilez.TileSpecs.parentX
The x-coordinate w.r.t. parent tile
# tilez.TileSpecs.parentY
The y-coordinate w.r.t. parent tile
# tilez.TileSpecs.innerPadding
Padding between children tiles
# tilez.TileSpecs.outerPadding
Padding around children tiles
# tilez.TileSpecs.hAlign · ('left' | 'center' | 'right')
Horizontal alignment w.r.t. parent tile
# tilez.TileSpecs.vAlign · ('top' | 'center' | 'bottom')
Vertical alignment w.r.t. parent tile
# tilez.TileSpecs.aspectRatio
Aspect ratio (width / height) of tile
# tilez.LinearScale.domain(domain: [number, number]) · [default: [0, 1]
]
You can set a domain which will be mapped to the tile range. Domains are also supported, where upper bound is less than lower bound, e.g. for $xScale
or $yScale
using [1, 0]
will map 0
to full width or height and 1
to 0
.
# tilez.LinearScale.range(range: [number, number])
Even though $xScale
and $yScale
from a tile's context are coming with predefined ranges, such that they span the full width or height of a tile, you can override the range with this method.
# tilez.LinearScale.(x: number)
The class itself is directly callable. It computes the function value for a given x value.
# tilez.LinearScale.inv(y: number)
Method inv
computes the x value for a given y value w.r.t. the inverse function. This can be useful for working with coords of mouse position.