Local coordinates
Understanding how local coordinates work is key to understanding florence
. The Graphic
, Section
and Glyph
components can create local coordinate systems, and all other components are affected by local coordinate systems.
Creating local coordinate systems
As stated above, local coordinate systems are created with the Graphic
and Section
components. The following props are used to create local coordinate systems:
Prop | Required | Type(s) | Default | Unit(s) |
---|---|---|---|---|
scaleX | false |
Function |
undefined |
d3 scale |
scaleY | false |
Function |
undefined |
d3 scale |
coordinates | false |
CoordinateTransformation |
cartesian() |
- |
flipX | false |
Boolean |
false |
- |
flipY | false |
Boolean |
false |
- |
padding | false |
Number , Object |
undefined |
Pixel |
zoomIdentity | false |
Object |
undefined |
- |
The purpose of a local coordinate system is to convert numerical values, given to components within a Graphic
, Section
or Glyph
, to pixel values that end up on your screen. This is a process that happens in all visualizations, and is often referred to as scaling in the grammar of graphics. All marks and layers have so-called ‘positioning props’ for this purpose. Below is a schematic representation of how florence
converts local coordinates to pixel values:
(Made with draw.io)
The three main steps in this process are:
- Scaling
- Applying the coordinate transformation, if any
- Applying the ‘final’ transformation, which encompasses padding, zooming and flipping.
These three concepts will be explained in depth in the next paragraphs. While these can conceptually be seen as seperate steps, internally florence
tries to combine them for performance reasons.
Scaling
Basics
Scaling is the process of “mapping a dimension of abstract data to a visual representation”. In florence
, this is not always entirely true. The output of the scaling step is not necessarily the final ‘visual representation’ (a pixel value). If the user chooses to apply a coordinate transformation, the output of the scaling step goes through another step of processing. Or, if the user specifies the padding
, flipX
, flipY
or zoomIdentity
props, there will be another (‘final’) step before we obtain a pixel value. But for now, let’s forget about coordinate transformations and things like padding and flipping, and look at how scaling works in isolation.
Default coordinates
If the scaleX
and scaleY
props are not provided, florence
falls back to so-called ‘default coordinates’. These coordinates have an extent of [0, 1]
in both dimensions. What this means in practice is that an x-coordinate of 0
is all the way on the left, and 1
is all the way on the right, while 0.5
is exactly in the middle. For the y-coordinate, 0
would be the top and 1
the bottom. See the example below:
<script>
import { Graphic, PointLayer } from '@snlab/florence'
</script>
<Graphic width={200} height={200}>
<PointLayer
x={[0, 0, 1, 1, 0.5]}
y={[0, 1, 1, 0, 0.5]}
fill={['red', 'blue', 'green', 'orange', 'purple']}
radius={30}
/>
</Graphic>
Providing scales
To scale coordinates in a local coordinate system, pass a d3 scale to scaleX
or scaleY
. D3 scales are functions that map domains to ranges, where a domain is the ‘abstract data dimension’ and the range is the output dimension – in this case, pixels on your screen. In florence
, the range will be taken care of for you, so you only need to define the domain.
Say that we have the following data:
const points = { x: [0, 5, 10], y: [0, 5, 10] }
And we want to use this data to render three points that fit exactly in the Graphic
. One way of doing this would be to write a function that scales them down to the ‘default coordinates’ discussed above, by dividing all coordinates by 10:
const points = { x: [0, 0.5, 1], y: [0, 0.5, 1] }
But scaleX
and scaleY
provide an alternative method: creating a local coordinate system. If we want to use the original data dimensions, we can use our data directly in the Graphic
using d3’s scaleLinear. We will also give the Graphic
a background color. This makes it easier to see where the points end up.
<script>
import { Graphic, PointLayer } from '@snlab/florence'
import { scaleLinear } from 'd3-scale'
const points = { x: [0, 5, 10], y: [0, 5, 10] }
</script>
<Graphic
width={200}
height={200}
scaleX={scaleLinear().domain([0, 10])}
scaleY={scaleLinear().domain([0, 10])}
backgroundColor="#b2ffb2"
>
<PointLayer x={points.x} y={points.y} radius={10} fill="red" />
</Graphic>
Non-numeric data
Scaling can also be used to convert String
s or Date
s, as long as the correct d3 scale is chosen.
<script>
import { Graphic, Rectangle } from '@snlab/florence'
import { scalePoint, scaleTime } from 'd3-scale'
const domainX = ['a', 'b', 'c', 'd', 'e']
const domainY = [new Date(2020, 1, 1), new Date(2020, 1, 10)])
</script>
<Graphic
width={200}
height={200}
scaleX={scalePoint().domain(domainX)}
scaleY={scaleTime().domain(domainY)}
>
<Rectangle
x1={'a'} x2={'e'}
y1={new Date(2020, 1, 1)} y2={new Date(2020, 1, 5)}
fill={'blue'}
opacity={0.5}
/>
<Rectangle
x1={'b'} x2={'d'}
y1={new Date(2020, 1, 3)} y2={new Date(2020, 1, 10)}
fill={'red'}
opacity={0.5}
/>
</Graphic>
Domain shorthand
It is also possible to simply pass a numeric domain to scaleX
or scaleY
. In this case, florence
will create a linear scale from the domain. Therefore this:
<Graphic scaleX={[0, 10]}>
is equal to this:
<Graphic scaleX={scaleLinear().domain([0, 10])}>
Function syntax
Even when scaleX
and scaleY
are used, it is possible to bypass them and position something in default coordinates. This is useful for, for example, annotations. To do this, pass the positioning prop a function that returns a default coordinate, instead of a value in the local coordinate system:
<script>
import { Graphic, Point, Label } from '@snlab/florence'
import { scaleLinear } from 'd3-scale'
const point = { x: 5, y: 5 }
</script>
<Graphic
width={200}
height={200}
scaleX={scaleLinear().domain([0, 10])}
scaleY={scaleLinear().domain([0, 10])}
backgroundColor={'#b2ffb2'}
>
<Point x={point.x} y={point.y} radius={20} fill={'red'} />
<Label x={() => 0.5} y={() => 0.5} text={'I am in the middle!'} />
</Graphic>
Coordinate transformation
Right now, florence
only supports two coordinate transformations: cartesian
(which is the default), and polar
. Coordinate systems are set using the coordinates
prop. For example:
<script>
import { Graphic, polar } from '@snlab/florence'
</script>
<Graphic
width={500}
height={500}
coordinates={polar()}
>
For more information on polar coordinates, see the polar coordinates documentation.
Final transformation
The last step in going from local coordinates to pixel values is the ‘final’ transformation. Again, while florence
internally tries to combine different steps to improve performance, conceptually this ‘final’ step can be divided into three sub-steps: flipping, padding and zooming.
Flipping
‘Flipping’ refers to using the flipX
and flipY
props to ‘flip’ to inverse the final output. flipY
is particularly useful. Web coordinates run from top to bottom by default, but when making graphics, we usually expect coordinates to run from bottom to top. flipY
provides a convenient way to invert this behavior.
Without using flipX
and flipY
, the following point will be in the top left corner:
...
<Graphic
width={200}
height={200}
backgroundColor={'#b2ffb2'}
>
<Point x={0.05} y={0.05} radius={10} />
</Graphic>
With flipX
and flipY
, it is moved to the bottom right corner instead:
...
<Graphic
width={200}
height={200}
flipX
flipY
backgroundColor={'#b2ffb2'}
>
<Point x={0.05} y={0.05} radius={10} />
</Graphic>
Padding
Padding is used to create space between the Graphic
or Section
’s outer borders and their contents. The padding
prop can be used in to ways: by giving it a Number
or an Object
. If given an Object
, it must have the following structure:
{
left: <Number>,
right: <Number>,
top: <Number>,
bottom: <Number>
}
If given a Number
, for example 20
, the padding will internally be converted to
{
left: 20,
right: 20,
top: 20,
bottom: 20
}
Furthermore, just providing some of the four possible members of the padding object, e.g. only left
:
{ left: 10 }
will set the rest to zero:
{
left: 10,
right: 0,
top: 0,
bottom: 0
}
An example:
<script>
import { Graphic, Section, Rectangle } from '@snlab/florence'
</script>
<Graphic
width={200}
height={200}
backgroundColor={'blue'}
>
<Section padding={25}>
<Rectangle x1={0} x2={1} y1={0} y2={1} fill={'red'} />
</Section>
</Graphic>
Zooming
Graphic
s, Section
s and Glyph
s can be zoomed using the zoomIdentity
prop. zoomIdentity
has to be an Object
with the following structure:
{
x: <Number>,
y: <Number>,
kx: <Number>,
ky: <Number>
}
Where x
is the translation in the x dimension (higher = further to the right), y
is the translation in the y dimension (higher = further down),
and kx
and ky
are the zooming factors in respectively the x and y dimensions. x
and y
are in pixel values. The identity transformation is
{
x: 0,
y: 0,
kx: 1,
ky: 1
}
An example that uses a tweened
store for zooming:
<script>
import { Graphic, Section, RectangleLayer } from '@snlab/florence'
import { scaleLinear } from 'd3-scale'
import { tweened } from 'svelte/motion'
import { cubicOut } from 'svelte/easing'
const values = Array(5).fill(0).map((_, i) => i)
const x1 = values.reduce(acc => [...acc, ...values], [])
const x2 = x1.map(v => v + 1)
const y1 = values.reduce((acc, v) => [...acc, ...Array(5).fill(v)], [])
const y2 = y1.map(v => v + 1)
const scale = scaleLinear().domain([0, 8]).range(['blue', 'red'])
const fill = x1.map((x, i) => scale(x + y1[i]))
const k = tweened(1, { easing: cubicOut })
setInterval(() => $k === 1 ? k.set(5) : k.set(1), 1400)
</script>
<Graphic
width={200}
height={200}
scaleX={[0, 5]}
scaleY={[0, 5]}
zoomIdentity={{ x: 0, y: 0, kx: $k, ky: $k }}
>
<RectangleLayer {x1} {x2} {y1} {y2} {fill} />
</Graphic>
Nesting Sections
In all the examples above, Graphic
components were used. While this is fine for simple graphics, more complicated graphics will often require multiple coordinate systems. This can be accomplished by using Section
s. Section
s can be positioned in the same way as Rectangle
s – see the Section documentation. The Section
can then be used to define a new local coordinate system. Section
s can even be nested. However, a Section
that has polar coordinate transformation cannot contain any other Section
s.
An example:
<script>
import { Graphic, Section, Point } from '@snlab/florence'
import { scaleLinear } from 'd3-scale'
</script>
<Graphic
width={500}
height={500}
padding={20}
>
<Section
y1={0}
y2={0.5}
scaleX={[0, 10]}
scaleY={[0, 10]}
backgroundColor={'#ffcccb'}
>
<Point x={5} y={5} radius={10} fill={'red'} />
<Section
x1={0}
x2={5}
y1={0}
y2={5}
scaleX={[0, 10]}
scaleY={[0, 10]}
backgroundColor={'#bcf5bc'}
>
<Point x={5} y={5} radius={10} fill={'green'} />
</Section>
</Section>
<Section
y1={0.5}
y2={1}
scaleX={[50, 60]}
scaleY={[50, 60]}
backgroundColor={'#add8e6'}
>
<Point x={55} y={55} radius={10} fill={'blue'} />
</Section>
</Graphic>