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:
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 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, 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:
- a set of instance methods that are available on the
Graphic
,Section
andGlyph
- the
onSelect
prop ofMark
s andLayer
s
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>