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 Mark
s and Layer
s:
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 Mark
s, Layer
s and Section
s. 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 Mark
s and Layer
s. These props are, in fact, three listeners in one:
start
, for when the end user starts draggingdrag
for during draggingend
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>