Interactivity

florence takes a different approach to interactivity than many other data visualization libraries. Where libraries like Vega Lite or G2 use a high-level ‘grammar’ specifically designed for interactivity, florence takes a more low-level approach. Instead of explaining to florence through a special language how an event (like a mouse click) should trigger an action (like a Mark changing color), florence relies on plain JavaScript combined with Svelte’s built-in reactivity system as much as possible. Although this approach can result in more boilerplate code, it does provide more control and flexibility for the end user to create tailored and innovative interactions. At the same time, many tricky aspects of interactivity have been abstracted away, so you do not need to reinvent the wheel.

Events and actions

To make interactivity easier to discuss, we will split up the concept into two parts:

  • Events
  • Actions

Events are things done by the end user of the graphic, while actions are things that the graphic does in response to events. All simple (highlighting, dragging) and more complex (zooming, selection, brushing) interactions can be broken down into events and actions. For example, an interaction where a Rectangle is highlighted by clicking on it, can be broken down in an event (end user clicking on a Rectangle mark) and an action (the Rectangle mark changing color). florence’s approach to events is to provide the user with a convenient API to capture (‘listen’ for) events triggered by the end user. This event listening API is discussed is the next paragraph. On the other hand, the number of actions that can be triggered as a result of events is potentially infinite- there is no exhaustive list of actions to document. There are, however, common interactions techniques that you might be familiar with from other libraries. While these interaction techniques are not supported out of the box with a single line of code, florence does provide handy abstractions for all the events and actions that these interaction techniques consist of. See the Highlighting, Dragging, Zooming and Selection paragraphs on this page for examples on how to set up these interactions.

Event listening

To listen for an event, simply pass a function to one of the event listening props of a Mark, Layer or Section:

<script>
  // ...

  function handleClick (event) {
    // ...
  }
</script>

<Point onClick={handleClick} />

Every time the end user clicks on this Point, the handleClick function will be called with an event object. The event object will be discussed below.

Mark and layer event listening props

The following mouse and click event listening props are available on all Marks and Layers:

Mouse events

Prop Description
onClick Fires when a user clicks on Mark or Layer
onMousedown Fires when a user presses the mouse button down while over a Mark or Layer
onMouseup Fires when a user releases the mouse button while over a Mark or Layer
onMouseover Fires once when a user moves the mouse over a Mark or Layer
onMouseout Fires once when a user moves the mouse out of a Mark or Layer
onMousedrag See ‘Dragging’ below

Touch events

Prop Description
onTouchdown Fires when a user touches a Mark or Layer
onTouchup Fires when a user lifts their finger from a Mark or Layer
onTouchover Fires when a user moves their finger over a Mark or Layer
onTouchout Fires when a user moves their finger out of the Mark or Layer
onTouchdrag See ‘Dragging’ below

Select events

Prop Description
onSelect See ‘Selection’ below
onDeselect See ‘Selection’ below

Graphic and Section event listening props

The following mouse and click event listening props are available on the Graphic and Section:

Mouse events

Prop Description
onClick Fires when a user clicks anywhere in the Graphic or Section
onWheel Fires when a user uses a mouse wheel or two-finger touchpad zoom while over a Graphic or Section
onMousedown Fires when a user presses the mouse button down while over a Graphic or Section
onMouseup Fires when a user releases the mouse button while over a Graphic or Section
onMouseover Fires once when a user moves the mouse over a Graphic or Section
onMouseout Fires once when a user moves the mouse out of a Graphic or Section
onMousemove Fires every time the mouse moves while it is over a Graphic or Section

Touch events

Prop Description
onPinch Fires when a user makes a ‘pinch’ gesture with two fingers
onTouchdown Fires when a user touches the area in a Graphic or Section
onTouchup Fires when a user lifts their finger from the Graphic or Section
onTouchover Fires when a user moves their finger into the Graphic or Section
onTouchout Fires when a user moves their finger out of the Graphic or Section
onTouchmove Fires every time the user moves their finger within the Graphic or Section

The event object

The event is nearly the same for Marks, Layers and Sections. The differences will be discussed here. For all three, the event object has the following structure:

{
  altKey: <Boolean>,
  clientX: <Number>,
  clientY: <Number>,
  ctrlKey: <Boolean>,
  hitSource: <String>,
  localCoordinates: <Object>,
  nativeType: <String>,
  pageX: <Number>,
  pageY: <Number>,
  screenCoordinates: <Object>,
  screenX: <Number>,
  screenY: <Number>,
  shiftKey: <Boolean>,
  timeStamp: <Number>,
  type: <String>
}

In addition, the Mark’s event object also has:

{
  hitBbox: <Object>,
  markType: <String>
}

While the Layer’s event object also has:

{
  hitBbox: <Object>,
  markType: <String>,
  index: <Number>,
  key: <String>
}

The key will be undefined if nothing is passed to the Layer’s keys prop.

Highlighting

Highlighting is a technique that allows the end user to request additional information about observations of interest. For example, in this scatterplot, the a and b variables are mapped to the x and y dimensions of the points and are therefore visible to the user, but the name of each observation is not visible. By hovering the mouse over a point, its name attribute is requested, and displayed below the graphic:

<script>
  import { Graphic, Section, PointLayer } from '@snlab/florence'
  import { scaleLinear } from 'd3-scale'

  let selectedIndex = null

  const a = [1, 3, 5, 3]
  const b = [3, 1, 3, 5]
  const name = ['West', 'North', 'East', 'South']
</script>

<Graphic width={300} height={300} backgroundColor="#b2ffb2">

  <Section 
    scaleX={scaleLinear().domain([1, 5])}
    scaleY={scaleLinear().domain([1, 5])}
    padding={20}
    clip={'outer'}
  >

    <PointLayer 
      x={a}
      y={b}
      radius={10}
      fill={({ index }) => index === selectedIndex ? 'red' : 'black' }
      onMouseover={({ index }) => selectedIndex = index}
      onMouseout={() => selectedIndex = null}
    />

  </Section>

</Graphic>

<h1 style="color: blue;">{selectedIndex === null ? 'None selected' : name[selectedIndex]}</h1>

None selected

Dragging

To make it easy to create dragging behavior, florence provides onMousedrag and onTouchdrag event listening props for all Marks and Layers. These props are, in fact, three listeners in one:

  • start, for when the end user starts dragging
  • drag for during dragging
  • end for when the end user stops dragging

Here is an example of how to use onMousedrag. onTouchdrag works similarly.

<script>
  import { Graphic, Section, Point } from '@snlab/florence'
  import { scaleLinear } from 'd3-scale'

  const points = new Array(5).fill(0).map(_ => ({
    x: Math.round(Math.random() * 10),
    y: Math.round(Math.random() * 10)
  }))

  $: center = {
    x: points.map(p => p.x).reduce((a, c) => a + c) / points.length,
    y: points.map(p => p.y).reduce((a, c) => a + c) / points.length
  }

  let blockReindexing

  function drag ({ dragType, localCoordinates }, index) {
    if (dragType === 'start') {
      blockReindexing = true
    }

    if (dragType === 'drag' && blockReindexing) {
      points[index] = localCoordinates
    }

    if (dragType === 'end') {
      blockReindexing = false
    }
  }
</script>

<Graphic width={300} height={300} {blockReindexing}>

  <Section 
    scaleX={scaleLinear().domain([0, 10])}
    scaleY={scaleLinear().domain([0, 10])}
    padding={20}
  >

    {#each points as point, i}
      <Point 
        {...point}
        radius={10}
        onMousedrag={e => drag(e, i)}
      />
    {/each}

    <Point {...center} radius={7} fill="blue" />
  
  </Section>

</Graphic>

blockReindexing

The blockReindexing prop is a necessary evil due to how florence works internally. By default, when any mark or layer becomes interactive (i.e., has a onMousedrag or event listener prop), the mark or layer will be added to a spatial index for rapid collision detection. When the coordinates of the mark or layer change, the spatial index has to be updated.

In this case, blockReindexing is used when the user is dragging the mark using onMousedrag. Every time the drag event fires, the Point’s coordinates are updated, and thus florence’s internal spatial index needs to be updated as well. This is an expensive operation, and can cause significant lags – hence the need for blockReindexing, which blocks this behavior until the user is done dragging when set to false.

blockReindexing can be supplied to the individual Mark, or Layer for which you want to block re-indexing, but in nearly all cases it will be most convenient to apply it to the root Graphic, as demonstrated above. Once blockReindexing is set back to true, the element(s) in question will be re-indexed, and will continue to be re-indexed automatically whenever some part of their positioning changes, until it is set back to false.

Zooming and panning

See the zooming and panning documentation for an example of how to set up zooming and panning.

Selection

Rectangle selection example

<script>
  import { scaleLinear } from 'd3-scale'
  import { Graphic, Section, Point, Rectangle } from '@snlab/florence'

  let section
  let makingSelection = false
  let selectionRectangle

  function onMousedown ({ screenCoordinates }) {
    section.resetSelectRectangle()

    makingSelection = true
    const { x, y } = screenCoordinates
    selectionRectangle = { x1: x, x2: x, y1: y, y2: y }

    section.selectRectangle(selectionRectangle)
  }

  function onMousemove ({ screenCoordinates }) {
    if (makingSelection) {
      const { x, y } = screenCoordinates
      selectionRectangle.x2 = x
      selectionRectangle.y2 = y

      section.updateSelectRectangle(selectionRectangle)
    }
  }

  function onMouseup () {
    if (makingSelection) makingSelection = false
  }

  let pointInSelection = false
</script>

<Graphic width={300} height={300} scaleX={[0, 300]} scaleY={[0, 300]} backgroundColor="#b2ffb2">

  <Section
    bind:this={section}
    padding={30}
    scaleX={scaleLinear().domain([0, 10])}
    scaleY={scaleLinear().domain([0, 10])}
    {onMousedown}
    {onMousemove}
    {onMouseup}
  >

    <Point 
      x={5} y={5} radius={7}
      onSelect={() => { pointInSelection = true }}
      onDeselect={() => { pointInSelection = false }}
    />
  
  </Section>

  <!-- Selection rectangle -->
  {#if selectionRectangle}

    <Rectangle 
      {...selectionRectangle} 
      fill="red" opacity={0.2}
    />

  {/if}

</Graphic>

<h1 style="color: blue;">{ pointInSelection ? 'Point in selection!' : 'Point not in selection...' }</h1>

Point not in selection...

Polygon selection example

<script>
  import { scaleLinear } from 'd3-scale'
  import { Graphic, Section, Point, Polygon } from '@snlab/florence'

  let section
  let selecting = false
  let selectionPolygon

  function onMousedown ({ screenCoordinates }) {
    section.resetSelectPolygon()
    selectionPolygon = undefined

    section.startSelectPolygon(screenCoordinates)
    selecting = true
  }

  function onMousemove ({ screenCoordinates }) {
    if (selecting) {
      section.addPointToSelectPolygon(screenCoordinates)
      selectionPolygon = section.getSelectPolygon()
    }
  }

  function onMouseup () {
    selecting = false
  }

  let pointInSelection = false
</script>

<Graphic width={300} height={300} scaleX={[0, 300]} scaleY={[0, 300]} backgroundColor="#b2ffb2">

  <Section
    bind:this={section}
    padding={30}
    scaleX={scaleLinear().domain([0, 10])}
    scaleY={scaleLinear().domain([0, 10])}
    {onMousedown}
    {onMousemove}
    {onMouseup}
  >

    <Point 
      x={5} y={5} radius={7}
      onSelect={() => { pointInSelection = true }}
      onDeselect={() => { pointInSelection = false }}
    />
  
  </Section>

  <!-- Selection polygon -->
  {#if selectionPolygon}

    <Polygon 
      geometry={selectionPolygon} 
      fill="red" opacity={0.2}
    />

  {/if}

</Graphic>

<h1 style="color: blue;">{ pointInSelection ? 'Point in selection!' : 'Point not in selection...' }</h1>

Point not in selection...