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:

coordinates

(Made with draw.io)

The three main steps in this process are:

  1. Scaling
  2. Applying the coordinate transformation, if any
  3. 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 Strings or Dates, 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>
I am in the middle!

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

Graphics, Sections and Glyphs 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 Sections. Sections can be positioned in the same way as Rectangles – see the Section documentation. The Section can then be used to define a new local coordinate system. Sections can even be nested. However, a Section that has polar coordinate transformation cannot contain any other Sections.

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>