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

Tabelle 4-1. Einige wichtige nutzergenerierte Ereignistypen
Funktion Beschreibung

click

Eine beliebige Maustaste wird auf einem Element gedrückt und losgelassen.

mousemove

Die Maus wird bewegt, während sie sich über einem Element befindet.

mousedown, mouseup

Eine Maustaste wird über einem Element gedrückt oder losgelassen.

mouseenter, mouseleave

Der Mauszeiger wird auf ein Element oder von einem Element weg bewegt.

mouseover, mouseout

Der Mauszeiger wird auf oder von einem Element oder einem seiner Kinder bewegt.

keydown, keyup

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.

Tabelle 4-2. Einige wichtige Methoden, Variablen und Funktionen im Zusammenhang mit der Ereignisbehandlung (sel ist ein Selection-Objekt)
Funktion Beschreibung

sel.on( types, callback )

Fügt für jedes Element in der Auswahl einen Callback hinzu oder entfernt ihn. Das Argument typesmuss eine Zeichenkette sein, die aus einem oder mehreren Ereignistypnamen besteht, die durch Leerzeichen getrennt sind. Einem Ereignistyp kann ein Punkt und ein beliebiger Tag folgen, damit mehrere Handler für einen einzelnen Ereignistyp registriert werden können.

  • Wenn ein Callback angegeben wird, wird er als Event-Handler registriert; ein eventuell vorhandener Event-Handler wird zuerst entfernt.

  • Wenn das Callback-Argument null lautet, wird ein eventuell vorhandener Handler entfernt.

  • Wenn das Callback-Argument fehlt, wird der aktuell zugewiesene Handler zurückgegeben.

d3.event

Enthält das aktuelle Ereignis, falls vorhanden, als DOM Event Objekt.

d3.mouse( parent )

Gibt ein Array mit zwei Elementen zurück, das die Mauskoordinaten relativ zum angegebenen Elternteil enthält.

sel.dispatch( type )

Sendet ein benutzerdefiniertes Ereignis des angegebenen Typs an alle Elemente in der aktuellen Auswahl.

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" );             1
    var svg = d3.select( selector ).attr( "cursor", "crosshair" ) 2
        .on( "mousemove", function() {
            var pt = d3.mouse( svg.node() );                      3
            txt.attr( "x", 18+pt[0] ).attr( "y", 6+pt[1] )        4
                .text( "" + pt[0] + "," + pt[1] );
        } );
}
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!

2

Ä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).

3

Erhalte die Mauskoordinaten, relativ zur linken oberen Ecke des Elements <svg>, mit der Funktion d3.mouse().

4

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).

dfti 0401
Abbildung 4-1. Datenpunkte, die zum selben Datensatz gehören, werden gleichzeitig in beiden Feldern hervorgehoben, basierend auf dem Abstand der Punkte im linken Feld zum Mauszeiger.
Beispiel 4-2. Befehle für Abbildung 4-1
function makeBrush() {
    d3.csv( "dense.csv" ).then( function( data ) {                1
        var svg1 = d3.select( "#brush1" );                        2
        var svg2 = d3.select( "#brush2" );

        var sc1=d3.scaleLinear().domain([0,10,50])                3
            .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); 4
        var cs2 = drawCircles(svg2,data,d=>d["A"],d=>d["C"],sc2);

        svg1.call( installHandlers, data, cs1, cs2, sc1, sc2 );   5
    } );
}

function drawCircles( svg, data, accX, accY, sc ) {
    var color = sc(Infinity);                                     6
    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 ) {                  7
                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;                                 8
                return sc1(r); } );                               9

            cs2.attr( "fill", (d,i) => sc2( data[i]["r"] ) ); } ) 10

        .on( "mouseleave", function() {
            cs1.attr( "fill", sc1(Infinity) );                    11
            cs2.attr( "fill", sc2(Infinity) ); } );
}
1

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 und C bezeichnet sind.

2

Wähle die beiden Felder des Diagramms aus.

3

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).

4

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.

5

Rufe die Funktion installHandlers() auf, um die Ereignishandler zu registrieren. Diese Codezeile verwendet die Funktion call(), um die FunktioninstallHandlers() aufzurufen, und gibt die Auswahl svg1 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).

6

Zu Beginn werden die Kreise mit der "maximalen" Farbe gezeichnet. Um diese Farbe zu finden, bewerte die Farbskala bei positiver Unendlichkeit.

7

Berechne für jeden Punkt in der Tabelle links den Abstand zum Mauszeiger...

8

... und speichere sie als zusätzliche Spalte im Datensatz. (Auf diese Weise kommunizieren wir zwischen den beiden Feldern der Abbildung).

9

Gib die entsprechende Farbe aus dem Farbverlauf zurück.

10

Verwende die zusätzliche Spalte im Datensatz, um die Punkte im Feld rechts einzufärben.

11

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 )           1
        .attr( "fill", "none" ).attr( "stroke", "black" )
        .attr( "stroke-width", 10 ).attr( "stroke-opacity", 0.1 )
        .attr( "visibility", "hidden" );                          2

    var hotzone = svg.append( "rect" ).attr( "cursor", "none" )   3
        .attr( "x", 50 ).attr( "y", 50 )
        .attr( "width", 200 ).attr( "height", 200 )
        .attr( "visibility", "hidden" )                           4
        .attr( "pointer-events", "all" )

        .on( "mouseenter", function() {                           5
            cursor.attr( "visibility", "visible" ); } )

        .on( "mousemove", function() {                            6
            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) ); } )
}
1

In dieser Version wird der eigentliche Mauszeiger ausgeblendet und durch einen großen, teilweise undurchsichtigen Kreis ersetzt. Punkte innerhalb des Kreises werden hervorgehoben.

2

Zu Beginn ist der Kreis ausgeblendet. Er wird erst angezeigt, wenn der Mauszeiger in die "heiße Zone" kommt.

3

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.

4

Das Rechteck ist nicht sichtbar. DOM-Elemente, deren visibility -Attribut auf hidden gesetzt ist, empfangen standardmäßig keine Mauszeiger-Ereignisse. Um dies zu umgehen, muss das Attribut pointer-eventsexplizit gesetzt werden. (Eine andere Möglichkeit, ein Element unsichtbar zu machen, ist, sein fill-opacity auf 0 zu setzen. In diesem Fall muss das pointer-events Attribut nicht geändert werden).

5

Wenn die Maus in die "heiße Zone" eintritt, wird der undurchsichtige Kreis, der als Zeiger fungiert, angezeigt.

6

Die Event-Handler mousemove und mouseleave 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>
dfti 0402
Abbildung 4-2. Die anfängliche Konfiguration für das Drag-and-Drop-Verhalten

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 mouseupregistriert, 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()                                          1
        .on( "start", function() {                                2
            color = d3.select( this ).attr( "fill" );
            widget = d3.select( this ).attr( "fill", "lime" );
        } )
        .on( "drag", function() {                                 3
            var pt = d3.mouse( d3.select( this ).node() );
            widget.attr( "cx", pt[0] ).attr( "cy", pt[1] );
        } )
        .on( "end", function() {                                  4
            widget.attr( "fill", color );
            widget = undefined;
        } );

    drag( d3.select( "#dragdrop" ).selectAll( "circle" ) );       5
}
1

Erstelle ein drag Funktionsobjekt mit der Fabrikfunktion d3.drag() und rufe dann die Funktion on() für das zurückgegebene Funktionsobjekt auf, um die erforderlichen Rückrufe zu registrieren.

2

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 (als Selection) widget zu.

3

Der drag Handler ruft die aktuellen Mauskoordinaten ab und verschiebt den ausgewählten Kreis an diese Stelle.

4

Der end Handler stellt die Farbe des Kreises wieder her und löscht die aktivewidget.

5

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 derselben SelectionInstanz 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üsselwort function verwenden, du kannst keine Pfeilfunktion benutzen. Dies ist eine Einschränkung der JavaScript-Sprache (siehe Anhang C). Beispiele dafür findest du in der Funktion installHandlers() 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 auf hidden gesetzt ist oder deren fill und stroke auf none gesetzt sind, empfangen standardmäßig keine Mauszeigerereignisse. Mit und dem Attribut pointer-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):

  1. 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() oder insert()) - auch wenn sie zunächst unsichtbar sind! (Die Transition API ermöglicht es dir, Elemente zu ändern und zu entfernen, aber sie bietet keine Möglichkeit, Elemente als Teil des Übergangs zu erstellen).

  2. Wähle nun die Elemente aus, die du mit der bekannten Selection API ändern möchtest.

  3. Rufe transition() für diese Auswahl auf, um einen Übergang zu erstellen. Optional kannst du duration(), delay() oder ease() aufrufen, um das Verhalten besser zu steuern.

  4. Bestimme den gewünschten Endzustand wie gewohnt mit attr() oder style(). 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.

Tabelle 4-3. Funktionen zum Erstellen und Beenden einer Transition (sel ist ein Selection-Objekt; trans ist einTransition-Objekt)
Funktion Beschreibung

sel.transition( tag )

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 Transition Instanz (zur Synchronisierung von Übergängen) sein.

sel.interrupt( tag )

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).

trans.transition()

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.

trans.selection()

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.

Tabelle 4-4. Funktionen zur Konfiguration einer Transition oder zum Abrufen der aktuellen Einstellung, wenn sie ohne Argument aufgerufen werden (trans ist ein Transition-Objekt)
Funktion Beschreibung

trans.delay( value )

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 d gebundenen Daten und sein Index in der Auswahl i übergeben.

trans.duration( value )

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 d gebundenen Daten und sein Index in der Auswahl i übergeben.

trans.ease( fct )

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 d3.easeCubic(ein stückweise definiertes kubisches Polynom mit "slow-in, slow-out" Verhalten).

trans.on( type, handler )

Fügt einen Event-Handler für den Übergang hinzu. Der Typ muss start, end oder interrupt sein. Der Event-Handler wird an dem entsprechenden Punkt im Lebenszyklus des Übergangs aufgerufen. Diese Funktion verhält sich ähnlich wie die Funktion on()für ein Selection Objekt (siehe Tabelle 4-2).

Ü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.

dfti 0403
Abbildung 4-3. Wenn dieses Balkendiagramm mit einem neuen Datensatz aktualisiert wird, werden die Aktualisierungen nacheinander von links nach rechts vorgenommen.
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 ];    1
    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] ) );    2

    var svg = d3.select( "#stagger" );

    var scX = d3.scaleLinear().domain( [0,n] ).range( [50,540] ); 3
    var scY = d3.scaleLinear().domain( [0,mx] ).range( [250,50] );

    svg.selectAll( "line" ).data( ds1 ).enter().append( "line" )  4
        .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() {                                 5
        [ ds1, ds2 ] = [ ds2, ds1 ];                              6

        svg.selectAll( "line" ).data( ds1 )                       7
            .transition().duration( 1000 ).delay( (d,i)=>200*i )  8
            .attr( "y2", d=>scY(d) );                             9
    } );
}
1

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.

2

Finde die Anzahl der Datenpunkte und den maximalen Gesamtwert für beide Datensätze.

3

Zwei Skalenobjekte, die die Werte im Datensatz auf vertikale und ihre Indexpositionen im Array auf horizontale Pixelkoordinaten abbilden.

4

Erstelle das Balkendiagramm. Jeder "Balken" wird als dicke Linie (und nicht als <rect> Element) realisiert.

5

Registriere einen Event-Handler für "click" Ereignisse.

6

Tausche die Datensätze aus.

7

Binde den (aktualisierten) Datensatz ds1 an die Auswahl...

8

... 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.

9

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!)

Tabelle 4-5. Methoden zur Angabe von benutzerdefinierten Interpolatoren (trans ist ein Transition-Objekt)
Funktion Beschreibung

trans.attrTween( name, factory )

Setzt einen eigenen Interpolator für das benannte Attribut. Das zweite Argument muss eine Fabrikmethode sein, die einen Interpolator zurückgibt.

trans.styleTween( name, factory )

Setzt einen eigenen Interpolator für den benannten Stil. Das zweite Argument muss eine Fabrikmethode sein, die einen Interpolator zurückgibt.

trans.tween( tag, factory )

Legt einen benutzerdefinierten Interpolator fest, der bei Übergängen aufgerufen wird. Das erste Argument ist ein beliebiges Tag, um diesen Interpolator zu identifizieren, das zweite Argument muss eine Fabrikmethode sein, die einen Interpolator zurückgibt. Die Wirkung des Interpolators ist nicht eingeschränkt; er wird nur wegen seiner Nebeneffekte aufgerufen.

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).

Tabelle 4-6. Funktionen und Methoden zur Erstellung und Verwendung von Timern (t ist ein Timer-Objekt)
Funktion Beschreibung

d3.timer( callback, after, start )

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 start kann den von d3.now() zurückgegebenen Zeitstempel enthalten, zu dem der Timer beginnen soll (der Standardwert ist jetzt). Das numerische Argument after kann eine Verzögerung in Millisekunden enthalten, die zur Startzeit addiert wird (Standardwert: 0).

d3.timeout( callback, after, start )

Wie d3.timer(), mit dem Unterschied, dass der Callback genau einmal aufgerufen wird.

d3.interval( callback, interval, start )

Ähnlich wie d3.timer(), mit dem Unterschied, dass der Callback nur alle interval Millisekunden aufgerufen wird.

t.stop()

Hält diesen Timer an. Hat keine Auswirkungen, wenn der Timer bereits angehalten wurde.

d3.now()

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 > 120e3 ) { timer.stop(); } // after 120 seconds
    } );
}
dfti 0404
Abbildung 4-4. Animationen: eine Lissajous-Figur (links, siehe Beispiel 4-6), und ein Wählermodell (rechts, siehe Beispiel 4-7)

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)                                      1
        .map( d => { return { x: d%n, y: d/n|0,
                              val: Math.random() } } );

    var sc = d3.scaleQuantize()                                   2
        .range( [ "white", "red", "black" ] );

    svg.selectAll( "rect" ).data( data ).enter().append( "rect" ) 3
        .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() {                                           4
        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() {                                     5
        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 );
}
1

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.)

2

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).

3

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.

4

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).

5

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 Funktion update()alle dt 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.

5 Siehe http://mathworld.wolfram.com/LissajousCurve.html.

6 Siehe http://mathworld.wolfram.com/VoterModel.html.

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.