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:

interface Event {
  altKey: boolean;
  clientX: number;
  clientY: number;
  ctrlKey: boolean;
  hitSource: string;
  localCoordinates: {
    x: any;
    y: any;
  };
  nativeType: string;
  pageX: number;
  pageY: number;
  screenCoordinates: {
    x: number;
    y: number;
  };
  screenX: number;
  screenY: number;
  shiftKey: boolean;
  timeStamp: number;
  type: string;
}

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

interface Event {
  hitBbox: Bbox;
  markType: string;
}

interface Bbox {
  minX: number;
  maxX: number;
  minY: number;
  maxY: number;
}

While the Layer’s event object also has:

interface Event {
  hitBbox: Bbox;
  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, PointLayer } from "@snlab/florence";

  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"
  scaleX={[1, 5]}
  scaleY={[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)}
  />

</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, Point } from "@snlab/florence";

  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,
  };

  function drag({ dragType, localCoordinates }, index) {
    if (dragType === "drag") {
      points[index] = localCoordinates;
    }
  }
</script>

<Graphic
  width={300}
  height={300}
  backgroundColor="#b2ffb2"
  scaleX={[0, 10]}
  scaleY={[0, 10]}
  padding={20}
>
  {#each points as point, i}
    <Point {...point} radius={10} onMousedrag={(e) => drag(e, i)} />
  {/each}
</Graphic>

Zooming and panning

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

Selection

florence currently supports two selection modes: rectangle and polygon. The selection API consists of two parts:

  1. a set of instance methods that are available on the Graphic, Section and Glyph
  2. the onSelect prop of Marks and Layers

Rectangle selection

For rectangle selection, the following Graphic/Section/Glyph instance methods are revelant:

interface InstanceMethods {
  resetSelectRectangle: () => void;
  selectRectangle: (rectangle: Rectangle) => void;
  updateSelectRectangle: (rectangle: Rectangle) => void;
}

interface Rectangle {
  x1: number;
  x2: number;
  y1: number;
  y2: number;
}

Note that the rectangle object passed to selectRectangle and updateSelectRectangle must be defined in pixels. For this reason, we use the screenCoordinates property of the event object in the onMousedown and onMousemove handlers. In the example below we also use pixels to display the select rectangle using the identity scale, which allows you to position marks in pixels.

<script>
	import { Graphic, Section, Point, Rectangle, identity } from '@moveSele/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} backgroundColor="#b2ffb2" scaleX={identity} scaleY={identity}>

  <Section
    bind:this={section}
    padding={30}
    scaleX={[0, 10]}
    scaleY={[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

For polygon selection, the following Graphic/Section/Glyph instance methods are revelant:

interface InstanceMethods {
  resetSelectPolygon: () => void;
  startSelectPolygon: (screenCoordinates: CoordinatePair) => void;
  addPointToSelectPolygon: (screenCoordinates: CoordinatePair) => void;
  moveSelectPolygon: (screenCoordinatesDelta: CoordinatePair) => void;
  getSelectPolygon: () => GeoJSON.Polygon
}

interface CoordinatePair {
  x: number;
  y: number;
}

Like with rectangle selection, all instance methods work with pixels / screen coordinates, including the GeoJSON.Polygon returned by getSelectPolygon.

<script>
	import { Graphic, Section, Point, Polygon, identity } 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} backgroundColor="#b2ffb2" scaleX={identity} scaleY={identity}>

  <Section
    bind:this={section}
    padding={30}
    scaleX={[0, 10]}
    scaleY={[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...