Kapitel 4. Ereignisse, Interaktivität und Animation
Diese Arbeit wurde mithilfe von KI übersetzt. Wir freuen uns über dein Feedback und deine Kommentare: translation-feedback@oreilly.com
Wenn sie von einem Browser gerendert werden, können SVG-Elemente Benutzerereignisse empfangen und als Ganzes manipuliert werden (z. B. um ihre Position oder ihr Aussehen zu ändern). Das bedeutet, dass sie sich im Wesentlichen wie Widgets in einem GUI-Toolkit verhalten. Das ist ein spannender Vorschlag: SVG als ein Widget-Set für Grafiken zu betrachten. In diesem Kapitel geht es um die Optionen, die zur Verfügung stehen, um die wesentlichen Merkmale einer Benutzeroberfläche zu erstellen: Interaktivität und Animation.
Veranstaltungen
Ein wichtiger Aspekt des DOM ist sein Ereignismodell: Im Grunde kann jedes DOM-Element Ereignisse empfangen und einen entsprechenden Handler aufrufen. Die Anzahl der verschiedenen Ereignistypen ist sehr groß; am wichtigsten für unsere Zwecke sind benutzergenerierte Ereignisse (Mausklicks oder -bewegungen sowie Tastendrücke; siehe Tabelle 4-1).1
Funktion | Beschreibung |
---|---|
|
Eine beliebige Maustaste wird auf einem Element gedrückt und losgelassen. |
|
Die Maus wird bewegt, während sie sich über einem Element befindet. |
|
Eine Maustaste wird über einem Element gedrückt oder losgelassen. |
|
Der Mauszeiger wird auf ein Element oder von einem Element weg bewegt. |
|
Der Mauszeiger wird auf oder von einem Element oder einem seiner Kinder bewegt. |
|
Eine beliebige Taste wird gedrückt oder losgelassen. |
D3 behandelt die Ereignisbehandlung als Teil von der Selection
Abstraktion (sieheTabelle 4-2). Wenn sel
eine Selection
Instanz ist, dann benutzt du die folgende Memberfunktion, um einen Callback als Event-Handler für den angegebenen Ereignistyp zu registrieren:
sel
.
on
(
type
,
callback
)
Das Argument type
muss eine Zeichenkette sein, die den Ereignistyp angibt (z. B."click"
). Jeder DOM-Ereignistyp ist zulässig. Wenn bereits ein Handler für den Ereignistyp über on()
registriert wurde, wird er entfernt, bevor der neue Handler registriert wird. Um den Handler für einen bestimmten Ereignistyp explizit zu entfernen, gib null
als zweites Argument an. Um mehrere Handler für denselben Ereignistyp zu registrieren, kann dem Typnamen ein Punkt und ein beliebiger Tag folgen (damit der Handler für "click.foo"
nicht den für "click.bar"
überschreibt).
Der Callback ist eine Funktion, die aufgerufen wird, wenn ein Ereignis des angegebenen Typs von einem beliebigen Element der Auswahl empfangen wird. Der Callback wird genauso aufgerufen wie jeder andere Accessor im Kontext einer Auswahl. Ihm werden der Datenpunkt d
, der an das aktuelle Element gebunden ist, der Index des Elements i
in der aktuellen Auswahl und die Knoten in der aktuellen Auswahl übergeben, während this
das aktuelle Element selbst enthält.2 Die eigentliche Ereignisinstanz wird dem Callback nicht als Argument übergeben, ist aber in der Variablen verfügbar:
d3
.
event
Wenn ein Ereignis eintritt, enthält diese Variable die rohe DOM-Ereignisinstanz (kein D3-Wrapper!). Die Informationen, die das Ereignisobjekt selbst liefert, hängen vom Ereignistyp ab. Bei Mausereignissen ist natürlich diePosition des Mauszeigers beim Auftreten des Ereignisses von besonderem Interesse. Das Ereignisobjekt enthält die Mauskoordinaten in Bezug auf drei verschiedene Koordinatensysteme,3 aber keines davon liefert direkt die Information, die am nützlichsten wäre, nämlich die Position in Bezug auf das enthaltende übergeordnete Element! Zum Glück kann man sie mit Hilfe von:
(((
"d3.mouse() function"
)))
d3
.
mouse
(
node
)
Diese Funktion gibt die Mauskoordinaten als Array mit zwei Elementen zurück [x, y]
. Das Argument sollte das umschließende Containerelement sein (als DOM Node
, nicht als Selection
). Wenn du mit SVG arbeitest, kannst du ein beliebiges Element angeben (als Node
). Die Funktion berechnet dann die Koordinaten relativ zum nächsten SVG-Element, das als Vorgänger dient.
Funktion | Beschreibung |
---|---|
|
Fügt für jedes Element in der Auswahl einen Callback hinzu oder entfernt ihn. Das Argument
|
|
Enthält das aktuelle Ereignis, falls vorhanden, als DOM |
|
Gibt ein Array mit zwei Elementen zurück, das die Mauskoordinaten relativ zum angegebenen Elternteil enthält. |
|
Diagramme mit der Maus erforschen
Für jemanden, der analytisch mit Daten arbeitet, bieten diese Funktionen einige spannende Möglichkeiten, denn sie machen es einfach, Diagramme interaktiv zu erkunden: Zeig mit der Maus auf eine Stelle im Diagramm und erhalte zusätzliche Informationen über den dort befindlichen Datenpunkt. Hier ist ein einfaches Beispiel. Wenn du die Funktion in Beispiel 4-1 aufrufst und dabei einen CSS-Selektorstring (siehe "CSS-Selektoren") angibst, der ein Element von <svg>
identifiziert, wird die aktuelle Position des Mauszeigers (in Pixelkoordinaten) im Diagramm selbst angezeigt. Außerdem ist die Position der Textanzeige nicht festgelegt, sondern bewegt sich zusammen mit dem Mauszeiger.
Beispiel 4-1. Wenn du einen CSS-Selektorstring angibst, zeigt diese Funktion kontinuierlich die Mausposition in Pixelkoordinaten an, wenn der Benutzer die Maus bewegt.
function
coordsPixels
(
selector
)
{
var
txt
=
d3
.
select
(
selector
)
.
append
(
"text"
)
;
var
svg
=
d3
.
select
(
selector
)
.
attr
(
"cursor"
,
"crosshair"
)
.
on
(
"mousemove"
,
function
(
)
{
var
pt
=
d3
.
mouse
(
svg
.
node
(
)
)
;
txt
.
attr
(
"x"
,
18
+
pt
[
0
]
)
.
attr
(
"y"
,
6
+
pt
[
1
]
)
.
text
(
""
+
pt
[
0
]
+
","
+
pt
[
1
]
)
;
}
)
;
}
Erstelle das Element
<text>
, um die Koordinaten anzuzeigen. Es ist wichtig, dies außerhalb des Event-Callbacks zu tun, da sonst jedes Mal, wenn der Benutzer die Maus bewegt, ein neues<text>
Element erstellt wird!Ändere die Form des Mauszeigers, während du dich über dem Element
<svg>
befindest. Das ist natürlich nicht erforderlich, aber es ist ein passender Effekt (und zeigt außerdem, wie der Mauszeiger durch Attribute verändert werden kann; siehe Anhang B).Erhalte die Mauskoordinaten, relativ zur linken oberen Ecke des Elements
<svg>
, mit der Funktiond3.mouse()
.Aktualisiere das zuvor erstellte Textelement. In diesem Beispiel werden sowohl der angezeigte Textinhalt des Elements als auch seine Position aktualisiert: etwas nach rechts von der Mausposition.
Die Anzeige der Mauskoordinaten ist natürlich weder neu noch besonders spannend. Aber es ist spannend zu sehen, wie einfach es ist, ein solches Verhalten in D3 zu implementieren!
Fallstudie: Gleichzeitiges Hervorheben
Das nächste Beispiel ist noch interessanter. Es befasst sich mit einem häufigen Problem bei der Arbeit mit multivariaten Datensätzen: Wie kann man zwei verschiedene Ansichten oder Projektionen der Daten visuell miteinander verbinden? Eine Möglichkeit ist, einen Bereich von Datenpunkten in einer Ansicht mit der Maus auszuwählen und gleichzeitig die entsprechenden Punkte in allen anderen Ansichten zu markieren. In Abbildung 4-1 werden die Punkte in beiden Ansichten entsprechend ihrer Entfernung (in Pixelkoordinaten) vom Mauszeiger in der linken Ansicht hervorgehoben. Da dieses Beispiel etwas komplizierter ist, werden wir zunächst eine vereinfachte Version besprechen (siehe Beispiel 4-2).
Beispiel 4-2. Befehle für Abbildung 4-1
function
makeBrush
(
)
{
d3
.
csv
(
"dense.csv"
)
.
then
(
function
(
data
)
{
var
svg1
=
d3
.
select
(
"#brush1"
)
;
var
svg2
=
d3
.
select
(
"#brush2"
)
;
var
sc1
=
d3
.
scaleLinear
(
)
.
domain
(
[
0
,
10
,
50
]
)
.
range
(
[
"lime"
,
"yellow"
,
"red"
]
)
;
var
sc2
=
d3
.
scaleLinear
(
)
.
domain
(
[
0
,
10
,
50
]
)
.
range
(
[
"lime"
,
"yellow"
,
"blue"
]
)
;
var
cs1
=
drawCircles
(
svg1
,
data
,
d
=>
d
[
"A"
]
,
d
=>
d
[
"B"
]
,
sc1
)
;
var
cs2
=
drawCircles
(
svg2
,
data
,
d
=>
d
[
"A"
]
,
d
=>
d
[
"C"
]
,
sc2
)
;
svg1
.
call
(
installHandlers
,
data
,
cs1
,
cs2
,
sc1
,
sc2
)
;
}
)
;
}
function
drawCircles
(
svg
,
data
,
accX
,
accY
,
sc
)
{
var
color
=
sc
(
Infinity
)
;
return
svg
.
selectAll
(
"circle"
)
.
data
(
data
)
.
enter
(
)
.
append
(
"circle"
)
.
attr
(
"r"
,
5
)
.
attr
(
"cx"
,
accX
)
.
attr
(
"cy"
,
accY
)
.
attr
(
"fill"
,
color
)
.
attr
(
"fill-opacity"
,
0.4
)
;
}
function
installHandlers
(
svg
,
data
,
cs1
,
cs2
,
sc1
,
sc2
)
{
svg
.
attr
(
"cursor"
,
"crosshair"
)
.
on
(
"mousemove"
,
function
(
)
{
var
pt
=
d3
.
mouse
(
svg
.
node
(
)
)
;
cs1
.
attr
(
"fill"
,
function
(
d
,
i
)
{
var
dx
=
pt
[
0
]
-
d3
.
select
(
this
)
.
attr
(
"cx"
)
;
var
dy
=
pt
[
1
]
-
d3
.
select
(
this
)
.
attr
(
"cy"
)
;
var
r
=
Math
.
hypot
(
dx
,
dy
)
;
data
[
i
]
[
"r"
]
=
r
;
return
sc1
(
r
)
;
}
)
;
cs2
.
attr
(
"fill"
,
(
d
,
i
)
=>
sc2
(
data
[
i
]
[
"r"
]
)
)
;
}
)
.
on
(
"mouseleave"
,
function
(
)
{
cs1
.
attr
(
"fill"
,
sc1
(
Infinity
)
)
;
cs2
.
attr
(
"fill"
,
sc2
(
Infinity
)
)
;
}
)
;
}
Lade den Datensatz und gib den Callback an, der aufgerufen werden soll, wenn die Daten verfügbar sind (siehe Kapitel 6 für weitere Informationen zum Abrufen von Daten). Die Datei enthält drei Spalten, die mit
A
,B
undC
bezeichnet sind.Wähle die beiden Felder des Diagramms aus.
D3 kann nahtlos zwischen den Farben interpolieren. Hier erstellen wir zwei Farbverläufe (einen für jedes Feld). (In Kapitel 7 erfährst du mehr über Interpolation und Skalierungsobjekte).
Erstelle die Kreise, die Datenpunkte darstellen. Die neu erstellten Kreise werden als
Selection
Objekte zurückgegeben. Einer allgemeinen D3-Konvention folgend, werden die Spalten im Funktionsaufruf durch die Bereitstellung von Accessor-Funktionen angegeben.Rufe die Funktion
installHandlers()
auf, um die Ereignishandler zu registrieren. Diese Codezeile verwendet die Funktioncall()
, um die FunktioninstallHandlers()
aufzurufen, und gibt die Auswahlsvg1
und die übrigen Parameter als Argumente an. (Das haben wir bereits in Beispiel 2-6 gesehen; siehe auch die Diskussion über Komponenten in Kapitel 5).Zu Beginn werden die Kreise mit der "maximalen" Farbe gezeichnet. Um diese Farbe zu finden, bewerte die Farbskala bei positiver Unendlichkeit.
Berechne für jeden Punkt in der Tabelle links den Abstand zum Mauszeiger...
... und speichere sie als zusätzliche Spalte im Datensatz. (Auf diese Weise kommunizieren wir zwischen den beiden Feldern der Abbildung).
Gib die entsprechende Farbe aus dem Farbverlauf zurück.
Verwende die zusätzliche Spalte im Datensatz, um die Punkte im Feld rechts einzufärben.
Stellt die ursprünglichen Farben der Punkte wieder her, wenn die Maus das linke Feld verlässt.
Diese Version des Programms funktioniert gut und löst das ursprüngliche Problem. Die verbesserte Version der Funktion installHandlers()
inBeispiel 4-3 ermöglicht es uns, einige zusätzliche Techniken beim Schreiben dieser Art von Benutzeroberflächencode zu diskutieren.
Beispiel 4-3. Eine verbesserte Version der Funktion installHandlers() aus Beispiel 4-2
function
installHandlers2
(
svg
,
data
,
cs1
,
cs2
,
sc1
,
sc2
)
{
var
cursor
=
svg
.
append
(
"circle"
)
.
attr
(
"r"
,
50
)
.
attr
(
"fill"
,
"none"
)
.
attr
(
"stroke"
,
"black"
)
.
attr
(
"stroke-width"
,
10
)
.
attr
(
"stroke-opacity"
,
0.1
)
.
attr
(
"visibility"
,
"hidden"
)
;
var
hotzone
=
svg
.
append
(
"rect"
)
.
attr
(
"cursor"
,
"none"
)
.
attr
(
"x"
,
50
)
.
attr
(
"y"
,
50
)
.
attr
(
"width"
,
200
)
.
attr
(
"height"
,
200
)
.
attr
(
"visibility"
,
"hidden"
)
.
attr
(
"pointer-events"
,
"all"
)
.
on
(
"mouseenter"
,
function
(
)
{
cursor
.
attr
(
"visibility"
,
"visible"
)
;
}
)
.
on
(
"mousemove"
,
function
(
)
{
var
pt
=
d3
.
mouse
(
svg
.
node
(
)
)
;
cursor
.
attr
(
"cx"
,
pt
[
0
]
)
.
attr
(
"cy"
,
pt
[
1
]
)
;
cs1
.
attr
(
"fill"
,
function
(
d
,
i
)
{
var
dx
=
pt
[
0
]
-
d3
.
select
(
this
)
.
attr
(
"cx"
)
;
var
dy
=
pt
[
1
]
-
d3
.
select
(
this
)
.
attr
(
"cy"
)
;
var
r
=
Math
.
hypot
(
dx
,
dy
)
;
data
[
i
]
[
"r"
]
=
r
;
return
sc1
(
r
)
;
}
)
;
cs2
.
attr
(
"fill"
,
(
d
,
i
)
=>
sc2
(
data
[
i
]
[
"r"
]
)
)
;
}
)
.
on
(
"mouseleave"
,
function
(
)
{
cursor
.
attr
(
"visibility"
,
"hidden"
)
;
cs1
.
attr
(
"fill"
,
sc1
(
Infinity
)
)
;
cs2
.
attr
(
"fill"
,
sc2
(
Infinity
)
)
;
}
)
}
In dieser Version wird der eigentliche Mauszeiger ausgeblendet und durch einen großen, teilweise undurchsichtigen Kreis ersetzt. Punkte innerhalb des Kreises werden hervorgehoben.
Zu Beginn ist der Kreis ausgeblendet. Er wird erst angezeigt, wenn der Mauszeiger in die "heiße Zone" kommt.
Die "heiße Zone" wird als Rechteck innerhalb des linken Bedienfelds definiert. Die Ereignishandler werden für dieses Rechteck registriert, d. h. sie werden nur aufgerufen, wenn sich der Mauszeiger innerhalb dieses Rechtecks befindet.
Das Rechteck ist nicht sichtbar. DOM-Elemente, deren
visibility
-Attribut aufhidden
gesetzt ist, empfangen standardmäßig keine Mauszeiger-Ereignisse. Um dies zu umgehen, muss das Attributpointer-events
explizit gesetzt werden. (Eine andere Möglichkeit, ein Element unsichtbar zu machen, ist, seinfill-opacity
auf0
zu setzen. In diesem Fall muss daspointer-events
Attribut nicht geändert werden).Wenn die Maus in die "heiße Zone" eintritt, wird der undurchsichtige Kreis, der als Zeiger fungiert, angezeigt.
Die Event-Handler
mousemove
undmouseleave
entsprechen denen in Beispiel 4-2, mit Ausnahme der zusätzlichen Befehle zur Aktualisierung des Kreises, der als Cursor fungiert.
Die Verwendung einer aktiven "Hot Zone" in diesem Beispiel ist natürlich optional, aber sie demonstriert eine interessante Technik. Gleichzeitig zeigt die Diskussion über das Attribut pointer-events
, dass diese Art der Programmierung von Benutzeroberflächen unerwartete Herausforderungen mit sich bringen kann. Wir werden nach dem nächsten Beispiel auf diesen Punkt zurückkommen.
Die D3 Drag-and-Drop-Verhaltenskomponente
Mehrere gängige Benutzeroberflächenmuster bestehen aus einer Kombination von Ereignissen und Reaktionen: Beim Drag-and-Drop-Muster beispielsweise wählt der Benutzer zunächst ein Element aus, verschiebt es dann und lässt es schließlich wieder los. D3 enthält eine Reihe vordefinierter Verhaltenskomponenten, die die Entwicklung von Code für solche Benutzeroberflächen vereinfachen, indem sie die erforderlichen Aktionen bündeln und organisieren. Darüber hinaus vereinheitlichen diese Komponenten auch einige Details der Benutzeroberfläche.
Betrachte eine Situation wie die in Abbildung 4-2, die den folgenden SVG-Ausschnitt zeigt:
<svg
id=
"dragdrop"
width=
"600"
height=
"200"
>
<circle
cx=
"100"
cy=
"100"
r=
"20"
fill=
"red"
/>
<circle
cx=
"300"
cy=
"100"
r=
"20"
fill=
"green"
/>
<circle
cx=
"500"
cy=
"100"
r=
"20"
fill=
"blue"
/>
</svg>
Jetzt wollen wir es dem Benutzer ermöglichen, die Position der Kreise mit der Maus zu verändern. Es ist nicht schwer, das bekannte Drag-and-Drop-Muster hinzuzufügen, indem man Callbacks für die Ereignisse mousedown
, mousemove
und mouseup
registriert, aber Beispiel 4-4 verwendet stattdessen die D3 drag
Verhaltenskomponente. Wie in Beispiel 2-6 erklärt, ist eine Komponente ein Funktionsobjekt, das eine Selection
Instanz als Argument nimmt und DOM-Elemente zu dieser Selection
hinzufügt (siehe auch Kapitel 5). Eine Verhaltenskomponente ist eine Komponente, die erforderliche Ereignis-Callbacks im DOM-Baum installiert. Gleichzeitig ist sie auch ein Objekt, das selbst über Mitgliedsfunktionen verfügt. Das Listing verwendet die Mitgliedsfunktion on( type, callback )
der Drag-Komponente, um die Callbacks für die verschiedenen Ereignistypen festzulegen.
Beispiel 4-4. Verwendung des Drag-and-Drop-Verhaltens
function
makeDragDrop
(
)
{
var
widget
=
undefined
,
color
=
undefined
;
var
drag
=
d3
.
drag
(
)
.
on
(
"start"
,
function
(
)
{
color
=
d3
.
select
(
this
)
.
attr
(
"fill"
)
;
widget
=
d3
.
select
(
this
)
.
attr
(
"fill"
,
"lime"
)
;
}
)
.
on
(
"drag"
,
function
(
)
{
var
pt
=
d3
.
mouse
(
d3
.
select
(
this
)
.
node
(
)
)
;
widget
.
attr
(
"cx"
,
pt
[
0
]
)
.
attr
(
"cy"
,
pt
[
1
]
)
;
}
)
.
on
(
"end"
,
function
(
)
{
widget
.
attr
(
"fill"
,
color
)
;
widget
=
undefined
;
}
)
;
drag
(
d3
.
select
(
"#dragdrop"
)
.
selectAll
(
"circle"
)
)
;
}
Erstelle ein
drag
Funktionsobjekt mit der Fabrikfunktiond3.drag()
und rufe dann die Funktionon()
für das zurückgegebene Funktionsobjekt auf, um die erforderlichen Rückrufe zu registrieren.Der
start
Handler speichert die aktuelle Farbe des ausgewählten Kreises, ändert dann die Farbe des ausgewählten Kreises und weist den ausgewählten Kreis selbst (alsSelection
)widget
zu.Der
drag
Handler ruft die aktuellen Mauskoordinaten ab und verschiebt den ausgewählten Kreis an diese Stelle.Der
end
Handler stellt die Farbe des Kreises wieder her und löscht die aktivewidget
.Schließlich rufst du die Komponentenoperation
drag
auf und gibst dabei eine Auswahl mit den Kreisen an, um die konfigurierten Ereignishandler auf der Auswahl zu installieren.
Ein idiomatischerer Weg, dies auszudrücken, wäre die Verwendung der Funktion call()
, anstatt die Komponentenoperation explizit aufzurufen:
d3
.
select
(
"#dragdrop"
).
selectAll
(
"circle"
)
.
call
(
d3
.
drag
()
.
on
(
"start"
,
function
()
{
...
}
)
.
on
(
"drag"
,
function
()
{
...
}
)
.
on
(
"end"
,
function
()
{
...
}
)
);
Die Ereignisnamen in Beispiel 4-4 mögen überraschen: Es handelt sich nicht um Standard-DOM-Ereignisse, sondern um D3-Pseudo-Ereignisse. Das D3-Verhalten drag
kombiniert die Behandlung von Maus- und Touchscreen-Ereignissen. Intern entspricht das Pseudo-Ereignis start
entweder einem mousedown
- oder einem touchstart
-Ereignis, und ähnlich verhält es sich mit drag
und end
. Außerdem verhindert das Verhalten drag
die Standardaktion des Browsers für bestimmte Ereignistypen.4
D3 enthält zusätzliche Verhaltensweisen, die beim Zoomen und bei der Auswahl von Teilen eines Diagramms mit der Maus helfen.
Hinweise zur Programmierung von Benutzeroberflächen
Ich hoffe, die bisherigen Beispiele haben dich davon überzeugt, dass die Erstellung interaktiver Graphen mit D3 nicht schwierig sein muss - ich glaube sogar, dass sie mit D3 sogar für einmalige Ad-hoc-Aufgaben und Erkundungen machbar ist. Gleichzeitig ist die Programmierung von grafischen Benutzeroberflächen, wie die Diskussion nach den beiden vorangegangenen Beispielen zeigt, immer noch ein relativ komplexes Problem. Viele Komponenten, jede mit ihren eigenen Regeln, sind beteiligt und können auf unerwartete Weise interagieren. Browser können sich in ihrer Umsetzung unterscheiden. Hier sind einige Hinweise und mögliche Überraschungen (siehe auch Anhang C für Hintergrundinformationen zur DOM-Ereignisbehandlung):
-
Wiederholte Aufrufe von
on()
für denselben Ereignistyp auf derselbenSelection
Instanz verwirren sich gegenseitig. Füge dem Ereignistyp einen eindeutigen Tag hinzu (getrennt durch einen Punkt), um mehrere Ereignisbehandler zu registrieren. -
Wenn du in einer Callback- oder Accessor-Funktion auf
this
zugreifen willst, musstdu das Schlüsselwortfunction
verwenden, du kannst keine Pfeilfunktion benutzen. Dies ist eine Einschränkung der JavaScript-Sprache (siehe Anhang C). Beispiele dafür findest du in der FunktioninstallHandlers()
in den Beispielen 4-2 und 4-3 und mehrmals inBeispiel 4-4. -
Das Standardverhalten des Browsers kann deinen Code beeinträchtigen; eventuell musst du es explizit verhindern.
-
Im Allgemeinen können nur sichtbare, gemalte Elemente Mauszeigerereignisse empfangen. Elemente, deren
visibility
Attribut aufhidden
gesetzt ist oder derenfill
undstroke
aufnone
gesetzt sind, empfangen standardmäßig keine Mauszeigerereignisse. Mit und dem Attributpointer-events
kannst du die Bedingungen für den Empfang von Mauszeigerereignissen genau festlegen (siehe MDN Pointer-Events). -
Ähnlich verhält es sich mit einem
<g>
Element, das keine visuelle Darstellung hat und daher keine Zeigerereignisse erzeugt. Dennoch kann es sinnvoll sein, einen Ereignishandler für ein<g>
Element zu registrieren, da Ereignisse, die von einem seiner (sichtbaren) Kinder erzeugt werden, an dieses delegiert werden. (Verwende ein unsichtbares Rechteck oder eine andere Form, um aktive "Hot Zones" zu definieren, wie in Beispiel 4-3.)
Sanfte Übergänge
Eine offensichtliche Möglichkeit, auf Ereignisse zu reagieren, besteht darin, das Aussehen oder die Konfiguration der Figur zu verändern (z. B. um einen Vorher-Nachher-Effekt zu zeigen). In diesem Fall ist es oft sinnvoll, die Veränderung nicht sofort, sondern schrittweise vorzunehmen, um die Aufmerksamkeit auf die Veränderung zu lenken und den Nutzern die Möglichkeit zu geben, zusätzliche Details zu erkennen. So können die Nutzer/innen zum Beispiel erkennen, welche Datenpunkte am stärksten von der Änderung betroffen sind und wie (siehe Abbildung 3-3 und Beispiel 3-1 für ein Beispiel).
Praktischerweise übernimmt die D3-Funktion Transition
die ganze Arbeit für dich. Sie repliziert den größten Teil der Selection
API, und du kannst das Aussehen ausgewählter Elemente mit attr()
oder style()
wie bisher ändern (siehe Kapitel 3). Jetzt werden die neuen Einstellungen aber nicht sofort wirksam, sondern schrittweise über eine konfigurierbare Zeitspanne (siehe Beispiel 2-8 für ein erstes Beispiel).
Im Verborgenen erstellt und plant D3 die erforderlichen Zwischenkonfigurationen, um den Anschein zu erwecken, dass sich der Graph über die gewünschte Dauer gleichmäßig verändert. Zu diesem Zweck ruft D3 einenInterpolator auf, der die Zwischenkonfigurationen zwischen den Start- und Endpunkten erstellt. Die Interpolationsfunktion von D3 ist ziemlich intelligent und in der Lage, automatisch zwischen den meisten Typen zu interpolieren (z. B. Zahlen, Datumsangaben, Farben, Strings mit eingebetteten Zahlen und mehr - siehe Kapitel 7 für eine detaillierte Beschreibung).
Übergänge erstellen und konfigurieren
Der Arbeitsablauf zur Erstellung eines Übergangs ist einfach (siehe auchTabelle 4-3):
-
Bevor du einen Übergang erstellst, musst du sicherstellen, dass alle Daten gebunden und alle Elemente, die Teil des Übergangs sein sollen, erstellt wurden (mit
append()
oderinsert()
) - auch wenn sie zunächst unsichtbar sind! (DieTransition
API ermöglicht es dir, Elemente zu ändern und zu entfernen, aber sie bietet keine Möglichkeit, Elemente als Teil des Übergangs zu erstellen). -
Wähle nun die Elemente aus, die du mit der bekannten
Selection
API ändern möchtest. -
Rufe
transition()
für diese Auswahl auf, um einen Übergang zu erstellen. Optional kannst duduration()
,delay()
oderease()
aufrufen, um das Verhalten besser zu steuern. -
Bestimme den gewünschten Endzustand wie gewohnt mit
attr()
oderstyle()
. D3 erstellt die Zwischenkonfigurationen zwischen den aktuellen Werten und den angegebenen Endzuständen und wendet sie für die Dauer des Übergangs an.
Oft sind diese Befehle Teil eines Event-Handlers, so dass der Übergang beginnt, wenn ein entsprechendes Ereignis eintritt.
Funktion | Beschreibung |
---|---|
|
Gibt einen neuen Übergang in der empfangenden Auswahl zurück. Das optionale Argument kann ein String (zur Identifizierung und Unterscheidung dieses Übergangs auf der Auswahl) oder eine |
|
Stoppt den aktiven Übergang und bricht alle anstehenden Übergänge auf den ausgewählten Elementen für den angegebenen Bezeichner ab. (Unterbrechungen werden nicht an die Kinder der ausgewählten Elemente weitergeleitet). |
|
Liefert einen neuen Übergang mit denselben ausgewählten Elementen wie der empfangende Übergang, der nach dem Ende des aktuellen Übergangs beginnen soll. Der neue Übergang erbt die Konfiguration des aktuellen Übergangs. |
|
Gibt die Auswahl für einen Übergang zurück. |
Neben dem gewünschten Endpunkt kannst du auf Transition
auch verschiedene Aspekte des Verhaltens konfigurieren (siehe Tabelle 4-4). Für alle diese Aspekte gibt es sinnvolle Standardeinstellungen, sodass eine explizite Konfiguration optional ist:
-
Eine Zeitspanne, die vergehen muss, bevor die Änderung in Kraft tritt.
-
Eine Dauer, in der sich die Einstellung schrittweise ändert.
-
Eine Erleichterung, die steuert, wie sich die Änderungsrate während der Übergangsdauer unterscheidet (um in die Animation hinein und aus ihr heraus zu erleichtern). Standardmäßig folgt das Easing einem stückweisen kubischen Polynom mit "slow-in, slow-out" Verhalten.
-
Einen Interpolator, um die Zwischenwerte zu berechnen (dies ist selten notwendig, da die Standard-Interpolatoren die meisten gängigen Konfigurationen automatisch verarbeiten).
-
Ein Event-Handler, um benutzerdefinierten Code auszuführen, wenn der Übergang beginnt, endet oder unterbrochen wird.
Funktion | Beschreibung |
---|---|
|
Legt die Verzögerung (in Millisekunden) fest, bevor der Übergang für jedes Element in der Auswahl beginnt; die Standardeinstellung ist 0. Die Verzögerung kann als Konstante oder als Funktion angegeben werden. Wenn es sich um eine Funktion handelt, wird die Funktion einmal für jedes Element aufgerufen, bevor der Übergang beginnt, und sollte die gewünschte Verzögerung zurückgeben. Der Funktion werden die an das Element |
|
Legt die Dauer (in Millisekunden) des Übergangs für jedes Element in der Auswahl fest; der Standardwert ist 250 Millisekunden. Die Dauer kann als Konstante oder als Funktion angegeben werden. Wenn es sich um eine Funktion handelt, wird die Funktion einmal für jedes Element aufgerufen, bevor der Übergang beginnt, und sollte die gewünschte Dauer zurückgeben. Der Funktion werden die an das Element |
|
Legt die Lockerungsfunktion für alle ausgewählten Elemente fest. Die Lockerung muss eine Funktion sein, die einen einzelnen Parameter zwischen 0 und 1 annimmt und einen einzelnen Wert, ebenfalls zwischen 0 und 1, zurückgibt. Die Standardlockerung ist |
|
Fügt einen Event-Handler für den Übergang hinzu. Der Typ muss |
Übergänge nutzen
Die Transition
API repliziert große Teile der Selection
API. Insbesondere sind alle Funktionen aus Tabelle 3-2 (d.h.select()
, selectAll()
und filter()
) verfügbar. AusTabelle 3-4 werden attr()
, style()
, text()
und each()
sowie alle Funktionen aus Tabelle 3-5 mit Ausnahme vonappend()
, insert()
und sort()
übernommen. (Wie bereits erwähnt, müssen alle Elemente, die an einem Übergang beteiligt sind, existieren, bevor der Übergang erstellt wird. Aus demselben Grund gibt es für Übergänge keine der Funktionen zur Datenbindung aus Tabelle 3-3 ).
Grundlegende Übergänge sind einfach zu verwenden, wie wir bereits in einem Beispiel in einem früheren Kapitel(Beispiel 3-1) gesehen haben. Die Anwendung in Beispiel 4-5 ist immer noch einfach, aber der Effekt ist ausgeklügelter: Ein Balkendiagramm wird mit neuen Daten aktualisiert, aber der Effekt istgestaffelt (mit delay()
), damit sich die Balken nicht alle gleichzeitig ändern.
Beispiel 4-5. Verwendung von Übergängen (siehe Abbildung 4-3)
function
makeStagger
(
)
{
var
ds1
=
[
2
,
1
,
3
,
5
,
7
,
8
,
9
,
9
,
9
,
8
,
7
,
5
,
3
,
1
,
2
]
;
var
ds2
=
[
8
,
9
,
8
,
7
,
5
,
3
,
2
,
1
,
2
,
3
,
5
,
7
,
8
,
9
,
8
]
;
var
n
=
ds1
.
length
,
mx
=
d3
.
max
(
d3
.
merge
(
[
ds1
,
ds2
]
)
)
;
var
svg
=
d3
.
select
(
"#stagger"
)
;
var
scX
=
d3
.
scaleLinear
(
)
.
domain
(
[
0
,
n
]
)
.
range
(
[
50
,
540
]
)
;
var
scY
=
d3
.
scaleLinear
(
)
.
domain
(
[
0
,
mx
]
)
.
range
(
[
250
,
50
]
)
;
svg
.
selectAll
(
"line"
)
.
data
(
ds1
)
.
enter
(
)
.
append
(
"line"
)
.
attr
(
"stroke"
,
"red"
)
.
attr
(
"stroke-width"
,
20
)
.
attr
(
"x1"
,
(
d
,
i
)
=>
scX
(
i
)
)
.
attr
(
"y1"
,
scY
(
0
)
)
.
attr
(
"x2"
,
(
d
,
i
)
=>
scX
(
i
)
)
.
attr
(
"y2"
,
d
=>
scY
(
d
)
)
;
svg
.
on
(
"click"
,
function
(
)
{
[
ds1
,
ds2
]
=
[
ds2
,
ds1
]
;
svg
.
selectAll
(
"line"
)
.
data
(
ds1
)
.
transition
(
)
.
duration
(
1000
)
.
delay
(
(
d
,
i
)
=>
200
*
i
)
.
attr
(
"y2"
,
d
=>
scY
(
d
)
)
;
}
)
;
}
Definiere zwei Datensätze. Der Einfachheit halber sind nur die y-Werte enthalten; wir werden den Array-Index jedes Elements für seine horizontale Position verwenden.
Finde die Anzahl der Datenpunkte und den maximalen Gesamtwert für beide Datensätze.
Zwei Skalenobjekte, die die Werte im Datensatz auf vertikale und ihre Indexpositionen im Array auf horizontale Pixelkoordinaten abbilden.
Erstelle das Balkendiagramm. Jeder "Balken" wird als dicke Linie (und nicht als
<rect>
Element) realisiert.Registriere einen Event-Handler für
"click"
Ereignisse.Tausche die Datensätze aus.
Binde den (aktualisierten) Datensatz
ds1
an die Auswahl...... und erstelle eine Übergangsinstanz. Jeder Balken braucht eine Sekunde, um seine neue Größe zu erreichen, beginnt aber erst nach einer Verzögerung. Die Verzögerung ist abhängig von der horizontalen Position jedes Balkens und wächst von links nach rechts. Das hat den Effekt, dass die "Aktualisierung" über das Diagramm hinweg zu laufen scheint.
Zum Schluss legst du die neue vertikale Länge jeder Linie fest. Dies ist der Endpunkt für den Übergang.
Tipps und Techniken
Nicht alle Übergänge sind so einfach wie die, die wir bisher gesehen haben. Hier sind einige zusätzliche Tipps und Techniken.
Strings
Die D3-Standardinterpolatoren interpolieren Zahlen, die in Zeichenketten eingebettet sind, lassen aber den Rest der Zeichenkette in Ruhe, weil es keine allgemein sinnvolle Möglichkeit gibt, zwischen Zeichenketten zu interpolieren. Der beste Weg, um einen sanften Übergang zwischen Zeichenketten zu erreichen, ist die Überblendung zwischenzwei Zeichenketten an der gleichen Stelle. Nehmen wir an, dass es zwei geeignete <text>
Elemente gibt:
<text
id=
"t1"
x=
"100"
y=
"100"
fill-opacity=
"1"
>
Hello</text>
<text
id=
"t2"
x=
"100"
y=
"100"
fill-opacity=
"0"
>
World</text>
Dann kannst du zwischen ihnen überblenden, indem du ihre Deckkraft änderst (und möglicherweise auch die Dauer des Übergangs):
d3
.
select
(
"#t1"
).
transition
().
attr
(
"fill-opacity"
,
0
);
d3
.
select
(
"#t2"
).
transition
().
attr
(
"fill-opacity"
,
1
);
Eine Alternative, die in bestimmten Fällen sinnvoll sein kann, ist das Schreiben eines eigenen Interpolators, der Zwischenwerte für Strings erzeugt.
Verkettete Übergänge
Übergänge können so verkettet werden, dass ein Übergang beginnt, wenn der erste endet. Die nachfolgenden Übergänge erben die Dauer und die Verzögerung des vorherigen Übergangs (es sei denn, sie werden explizit überschrieben). Der folgende Code färbt die ausgewählten Elemente zuerst rot und dann blau:
d3
.
selectAll
(
"circle"
)
.
transition
().
duration
(
2000
).
attr
(
"fill"
,
"red"
)
.
transition
().
attr
(
"fill"
,
"blue"
);
Explizite Startkonfiguration
Wenn du nicht vorhast, einen benutzerdefinierten Interpolator zu verwenden (siehe unten), ist es wichtig, dass die Startkonfiguration explizit festgelegt wird. Verlasse dich zum Beispiel nicht auf den Standardwert (black
) für das Attribut fill
: Wenn das Attribut fill nicht explizit gesetzt ist, weiß der Standard-Interpolator nicht, was er tun soll.
Benutzerdefinierte Interpolatoren
Mit den Methoden in Tabelle 4-5 kann ein benutzerdefinierter Interpolator festgelegt werden, der während des Übergangs verwendet wird. Die Methoden zum Festlegen eines benutzerdefinierten Interpolators nehmen eine Fabrikfunktion als Argument. Wenn der Übergang beginnt, wird die Fabrikfunktion für jedes Element in der Auswahl aufgerufen und bekommt die an das Element gebundenen Daten d
und den Index des Elements i
übergeben, wobei this
auf den aktuellen DOMNode
gesetzt wird. Die Factory muss eine Interpolator-Funktion zurückgeben. Die Interpolatorfunktion muss ein einzelnes numerisches Argument zwischen 0 und 1 akzeptieren und einen geeigneten Zwischenwert zwischen der Start- und der Endkonfiguration zurückgeben. Der Interpolator wird aufgerufen, nachdem das Easing angewendet wurde. Der folgende Code verwendet einen einfachen benutzerdefinierten Farbinterpolator ohne Easing (siehe Kapitel 8, um mehr über flexiblere Möglichkeiten zu erfahren, mit Farben in D3 zu arbeiten):
d3
.
select
(
"#custom"
).
selectAll
(
"circle"
)
.
attr
(
"fill"
,
"white"
)
.
transition
().
duration
(
2000
).
ease
(
t
=>
t
)
.
attrTween
(
"fill"
,
function
()
{
return
t
=>
"hsl("
+
360
*
t
+
", 100%, 50%)"
}
);
Das nächste Beispiel ist noch interessanter. Es erstellt ein Rechteck, das an der Position (100, 100) im Diagramm zentriert ist, und dreht das Rechteck dann gleichmäßig um seinen Mittelpunkt. (Die Standardinterpolatoren von D3 verstehen einige SVG-Transformationen, aber dieses Beispiel zeigt, wie du deinen eigenen Interpolator schreiben kannst, falls du ihn brauchst).
d3
.
select
(
"#custom"
).
append
(
"rect"
)
.
attr
(
"x"
,
80
).
attr
(
"y"
,
80
)
.
attr
(
"width"
,
40
).
attr
(
"height"
,
40
)
.
transition
().
duration
(
2000
).
ease
(
t
=>
t
)
.
attrTween
(
"transform"
,
function
()
{
return
t
=>
"rotate("
+
360
*
t
+
",100,100)"
}
);
Ereignisse im Übergang
Übergänge geben benutzerdefinierte Ereignisse aus, wenn sie beginnen, enden und unterbrochen werden. Mit der Methode on()
kannst du einen Event-Handler für einen Übergang registrieren, der aufgerufen wird, wenn das entsprechende Lebenszyklus-Ereignis ausgelöst wird. (Weitere Informationen findest du in der D3-Referenzdokumentation.
Easings
Mit , der Methode ease()
, kannst du eine Staffelung festlegen. Der Zweck eines Easings ist es, die Zeit, die der Interpolator sieht, zu "strecken" oder zu "komprimieren", so dass die Animation in die Bewegung "hinein- und wieder herausfließen" kann. Dadurch wird die visuelle Wirkung einer Animation oft erheblich verbessert. Tatsächlich haben die Disney-Animatoren die "Verlangsamung" als eines der "Zwölf Prinzipien der Animation" anerkannt (siehe "Prinzipien der Animation"). Aber manchmal, wenn sie nicht mit der Art und Weise übereinstimmen, wie der Benutzer das Verhalten eines Objekts erwartet, können Lockerungen geradezu verwirrend sein. Das Gleichgewicht ist definitiv subtil.
Ein easing nimmt einen Parameter im Intervall [0, 1] und bildet ihn auf dasselbe Intervall ab, wobei es für t = 0 bei 0 beginnt und für t = 1 bei 1 endet. Die Abbildung ist in der Regel nichtlinear (ansonsten ist das easing einfach die Identität). Die Standardabschwächung ist d3.easeCubic
, die eine Version des "slow-in, slow-out" Verhaltens implementiert.
Technisch gesehen ist ein Easing einfach eine Zuordnung, die auf den Zeitparameter t angewendet wird, bevor er an den Interpolator weitergegeben wird. Das macht die Unterscheidung zwischen dem Easing und dem Interpolator etwas willkürlich. Was ist, wenn ein benutzerdefinierter Interpolator selbst den Zeitparameter auf eine nichtlineare Weise verändert? Aus praktischer Sicht scheint es am besten zu sein, Easings als eine Komfortfunktion zu behandeln, die den Standardinterpolatoren ein "Slow-in, Slow-out"-Verhalten verleiht. (In D3 gibt es eine verwirrend große Auswahl an easings, von denen einige den Unterschied zwischen einem easing und einem benutzerdefinierten Interpolator deutlich verwischen).
Übertreibe es nicht mit den Übergängen
Übergänge können überstrapaziert werden. Ein generelles Problem bei Übergängen ist, dass sie in der Regel nicht vom Nutzer unterbrochen werden können: Die daraus resultierende erzwungene Wartezeit kann schnell zu Frustration führen. Wenn Übergänge eingesetzt werden, damit der Nutzer die Auswirkungen einer Änderung nachvollziehen kann, helfen sie beim Verständnis (siehe Abbildung 3-3 für ein einfaches Beispiel). Aber wenn sie nur "für den Effekt" eingesetzt werden, werden sie schnell ermüdend, sobald die anfängliche Niedlichkeit nachlässt.(Abbildung 4-3 kann in diesem Sinne als warnendes Beispiel dienen!)
Animation mit Timer-Ereignissen
Übergänge sind eine praktische Technik, um eine Konfiguration fließend in eine andere zu überführen, aber sie sind nicht als Rahmen für allgemeine Animationen gedacht. Um diese zu erstellen, ist es in der Regel notwendig, auf einer niedrigeren Ebene zu arbeiten. D3 enthält einen speziellen Timer, der einen bestimmten Callback einmal pro Animationsframe aufruft, d. h. jedes Mal, wenn der Browser dabei ist, den Bildschirm neu zu malen. Das Zeitintervall ist nicht konfigurierbar, da es von der Aktualisierungsrate des Browsers bestimmt wird (bei den meisten Browsern etwa 60 Mal pro Sekunde oder alle 17 Millisekunden). Es ist auch nicht exakt; dem Callback wird ein hochpräziser Zeitstempel übergeben, mit dem man feststellen kann, wie viel Zeit seit dem letzten Aufruf vergangen ist (siehe Tabelle 4-6).
Funktion | Beschreibung |
---|---|
|
Gibt eine neue Timer-Instanz zurück. Der Timer ruft den Callback dauerhaft einmal pro Animationsframe auf. Beim Aufruf wird dem Callback die scheinbar verstrichene Zeit seit Beginn des Timers übergeben (die scheinbar verstrichene Zeit läuft nicht weiter, solange sich das Fenster oder der Tab im Hintergrund befindet). Das numerische Argument |
|
Wie |
|
Ähnlich wie |
|
Hält diesen Timer an. Hat keine Auswirkungen, wenn der Timer bereits angehalten wurde. |
|
Gibt die aktuelle Zeit in Millisekunden zurück. |
Beispiel: Echtzeit-Animationen
Beispiel 4-6 erzeugt eine flüssige Animation, indem es bei jedem neuen Browserbild einen Graphen aktualisiert. Der Graph (siehe links inAbbildung 4-4) zeichnet eine Linie (eine Lissajous-Kurve5Im Gegensatz zu den meisten anderen Beispielen verwendet dieser Code kein Binding - vor allem, weil es keinen Datensatz zum Binden gibt! Stattdessen wird bei jedem Zeitschritt die nächste Position der Kurve berechnet und ein neues <line>
Element von der vorherigen bis zur neuen Position zum Diagramm hinzugefügt. Die Deckkraft aller Elemente wird um einen konstanten Faktor verringert, und Elemente, deren Deckkraft so weit gesunken ist, dass sie im Grunde unsichtbar sind, werden aus dem Diagramm entfernt. Der aktuelle Wert der Deckkraft wird in jedem DOM Node
selbst als neue, "unechte" Eigenschaft gespeichert. Dies ist optional; du könntest den Wert stattdessen in einer separaten Datenstruktur speichern, die von jedem Knoten verschlüsselt wird (zum Beispiel mit d3.local()
, die für diesen Zweck vorgesehen ist), oder den aktuellen Wert mit attr()
abfragen, aktualisieren und zurücksetzen.
Beispiel 4-6. Echtzeit-Animation (siehe die linke Seite von Abbildung 4-4)
function
makeLissajous
()
{
var
svg
=
d3
.
select
(
"#lissajous"
);
var
a
=
3.2
,
b
=
5.9
;
// Lissajous frequencies
var
phi
,
omega
=
2
*
Math
.
PI
/
10000
;
// 10 seconds per period
var
crrX
=
150
+
100
,
crrY
=
150
+
0
;
var
prvX
=
crrX
,
prvY
=
crrY
;
var
timer
=
d3
.
timer
(
function
(
t
)
{
phi
=
omega
*
t
;
crrX
=
150
+
100
*
Math
.
cos
(
a
*
phi
);
crrY
=
150
+
100
*
Math
.
sin
(
b
*
phi
);
svg
.
selectAll
(
"line"
)
.
each
(
function
()
{
this
.
bogus_opacity
*=
.
99
}
)
.
attr
(
"stroke-opacity"
,
function
()
{
return
this
.
bogus_opacity
}
)
.
filter
(
function
()
{
return
this
.
bogus_opacity
<
0.05
}
)
.
remove
();
svg
.
append
(
"line"
)
.
each
(
function
()
{
this
.
bogus_opacity
=
1.0
}
)
.
attr
(
"x1"
,
prvX
).
attr
(
"y1"
,
prvY
)
.
attr
(
"x2"
,
crrX
).
attr
(
"y2"
,
crrY
)
.
attr
(
"stroke"
,
"green"
).
attr
(
"stroke-width"
,
2
);
prvX
=
crrX
;
prvY
=
crrY
;
if
(
t
>
120
e3
)
{
timer
.
stop
();
}
// after 120 seconds
}
);
}
Beispiel: Glättung periodischer Aktualisierungen mit Übergängen
Im vorherigen Beispiel wurde jeder neue Datenpunkt, der angezeigt werden sollte, in Echtzeit berechnet. Das ist nicht immer möglich. Stell dir vor, du musst auf einen entfernten Server zugreifen, um Daten zu erhalten. Vielleicht willst du ihn regelmäßig abfragen, aber sicher nicht bei jedem neuen Bild. In jedem Fall ist ein Fernabruf immer ein asynchroner Aufruf und muss entsprechend behandelt werden.
In einer solchen Situation können Übergänge zu einem besseren Nutzererlebnis beitragen, indem sie die Zeiträume zwischen den Aktualisierungen der Datenquelle glätten. In Beispiel 4-7 wurde der Remote-Server durch eine lokale Funktion ersetzt, um das Beispiel einfach zu halten, aber die meisten Konzepte werden übernommen. Das Beispiel implementiert ein einfaches Wählermodell:6 In jedem Zeitschritt wählt jedes Graphelement zufällig einen seiner acht Nachbarn aus und nimmt dessen Farbe an. Die Aktualisierungsfunktion wird nur alle paar Sekunden aufgerufen; in der Zwischenzeit werden D3-Übergänge verwendet, um den Graphen reibungslos zu aktualisieren (siehe die rechte Seite von Abbildung 4-4).
Beispiel 4-7. Verwendung von Übergängen zur Glättung regelmäßiger Aktualisierungen (siehe die rechte Seite von Abbildung 4-4)
function
makeVoters
(
)
{
var
n
=
50
,
w
=
300
/
n
,
dt
=
3000
,
svg
=
d3
.
select
(
"#voters"
)
;
var
data
=
d3
.
range
(
n
*
n
)
.
map
(
d
=>
{
return
{
x
:
d
%
n
,
y
:
d
/
n
|
0
,
val
:
Math
.
random
(
)
}
}
)
;
var
sc
=
d3
.
scaleQuantize
(
)
.
range
(
[
"white"
,
"red"
,
"black"
]
)
;
svg
.
selectAll
(
"rect"
)
.
data
(
data
)
.
enter
(
)
.
append
(
"rect"
)
.
attr
(
"x"
,
d
=>
w
*
d
.
x
)
.
attr
(
"y"
,
d
=>
w
*
d
.
y
)
.
attr
(
"width"
,
w
-
1
)
.
attr
(
"height"
,
w
-
1
)
.
attr
(
"fill"
,
d
=>
sc
(
d
.
val
)
)
;
function
update
(
)
{
var
nbs
=
[
[
0
,
1
]
,
[
0
,
-
1
]
,
[
1
,
0
]
,
[
-
1
,
0
]
,
[
1
,
1
]
,
[
1
,
-
1
]
,
[
-
1
,
1
]
,
[
-
1
,
-
1
]
]
;
return
d3
.
shuffle
(
d3
.
range
(
n
*
n
)
)
.
map
(
i
=>
{
var
nb
=
nbs
[
nbs
.
length
*
Math
.
random
(
)
|
0
]
;
var
x
=
(
data
[
i
]
.
x
+
nb
[
0
]
+
n
)
%
n
;
var
y
=
(
data
[
i
]
.
y
+
nb
[
1
]
+
n
)
%
n
;
data
[
i
]
.
val
=
data
[
y
*
n
+
x
]
.
val
;
}
)
;
}
d3
.
interval
(
function
(
)
{
update
(
)
;
svg
.
selectAll
(
"rect"
)
.
data
(
data
)
.
transition
(
)
.
duration
(
dt
)
.
delay
(
(
d
,
i
)
=>
i
*
0.25
*
dt
/
(
n
*
n
)
)
.
attr
(
"fill"
,
d
=>
sc
(
d
.
val
)
)
}
,
dt
)
;
}
Erzeugt eine Reihe von n2 Objekten. Jedes Objekt hat einen Zufallswert zwischen 0 und 1 und kennt außerdem seine x- und y-Koordinaten in einem Quadrat. (Der ungerade
d/n|0
Ausdruck ist eine Abkürzung, um den Quotienten auf der linken Seite auf eine ganze Zahl abzuschneiden: Der bitweise ODER-Operator zwingt seine Operanden in eine ganzzahlige Darstellung und schneidet dabei die Dezimalstellen ab. Dies ist ein halbwegs gängiges JavaScript-Idiom, das man kennen sollte.)Das Objekt, das von
d3.scaleQuantize()
zurückgegeben wird, ist eine Instanz derBinning-Skala, die den Eingabebereich in gleich große Bins unterteilt. Hier wird der Standard-Eingabebereich[0,1]
in drei gleich große Bins unterteilt, einen für jede Farbe. (In Kapitel 7 findest du weitere Informationen zu Skalenobjekten).Bindet den Datensatz und erstellt dann ein Rechteck für jeden Datensatz im Datensatz. Jeder Datensatz enthält Informationen über die Position des Rechtecks, und das Skalenobjekt wird verwendet, um die Werteigenschaft jedes Datensatzes einer Farbe zuzuordnen.
Die eigentliche Aktualisierungsfunktion, die beim Aufruf eine neue Konfiguration berechnet. Sie besucht jedes Element des Arrays in zufälliger Reihenfolge. Für jedes Element wählt sie nach dem Zufallsprinzip einen seiner acht Nachbarn aus und ordnet den Wert des Nachbarn dem aktuellen Element zu. (Der Zweck der Arithmetik besteht darin, zwischen dem Array-Index des Elements und seinen(x, y)-Koordinaten in der Matrixdarstellung umzurechnen und dabei periodische Randbedingungen zu berücksichtigen: Wenn du die Matrix links verlässt, kommst du rechts wieder rein und umgekehrt; dasselbe gilt für oben und unten).
Die Funktion
d3.interval()
gibt einen Timer zurück, der den angegebenen Callback in einer konfigurierbaren Häufigkeit aufruft. In diesem Fall ruft sie die Funktionupdate()
alledt
Millisekunden auf und aktualisiert die Diagrammelemente mit den neuen Daten. Die Aktualisierungen werden durch einen Übergang geglättet, der entsprechend der Position des Elements im Array verzögert wird. Die Verzögerung ist kurz im Vergleich zur Dauer des Übergangs. Der Effekt ist, dass die Aktualisierung von oben nach unten über die Abbildung läuft.
1 Weitere Informationen findest du in der MDN Event Reference.
2 Wenn du in einem Callback auf this
zugreifen willst, musst du das Schlüsselwort function
verwenden, um den Callback zu definieren; du kannst keine Pfeilfunktion verwenden.
3 Sie sind: screen
client
, bezogen auf die Kanten des Browserfensters und page
, bezogen auf die Kanten des Dokuments selbst. Aufgrund der Platzierung des Fensters auf dem Bildschirm und des Scrollens der Seite innerhalb des Browsers unterscheiden sich diese drei Werte im Allgemeinen.
4 Wenn du versuchst, das aktuelle Beispiel zu implementieren, ohne die D3 drag
Funktion zu nutzen, kann es vorkommen, dass du ein falsches Verhalten der Benutzeroberfläche beobachtest. Das liegt wahrscheinlich an der Standardaktion des Browsers, die das beabsichtigte Verhalten beeinträchtigt. Abhilfe schaffst du, indem du d3.event.preventDefault()
im mousemove
Handler aufrufst. Weitere Informationen findest du in Anhang C.
Get D3 für die Ungeduldigen now with the O’Reilly learning platform.
O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.