1. Einleitung
D3.js (Data-Driven Documents) ist ein Framework für die Visualisierung von Daten im Web-Bereich. Anders als viele andere Frameworks liefert D3.js praktisch keine fixfertigen Diagramme, die nur sehr wenige Zeilen Code benötigen. Stattdessen erlaubt es D3.js, die Visualisierungen auf mannigfaltige Art zu konfigurieren, oder aus vorgegebenen Bausteinen komplett neue Diagramme zu entwerfen. Die gebotene Flexibilität hat allerdings den Preis einer erhöhten Komplexität. In diesem Beitrag möchte ich exemplarisch aufzeigen, wie mithilfe von D3.js ein sogenanntes Chord-Diagramm erstellt, und danach schrittweise optimiert werden kann. Dazu wird es auch nötig sein, einige kleine Teile des Quellcodes von D3.js selbst zu bearbeiten. Einige dieser Optimierungsschritte und zusätzlichen “Gimmicks” lassen sich auch auf andere Diagrammtypen anwenden. In diesem ersten Teil wird es darum gehen, das grundsätzliche Diagramm zu erhalten. In einem zweiten Teil wird das Diagramm dann optimiert und um Interaktivität ergänzt.
Chord-Diagramme sind typischerweise kreisförmige Diagramme, welche Zusammenhänge zwischen jeweils zwei Objekten darstellen können. Ein typisches Beispiel sind Personenflüsse, also wie viele Personen sich in einem bestimmten Zeitraum von einem Ort zu einem anderen bewegen. Als Beispiel in diesem Blogpost sollen die Personenflüsse zwischen den verschiedenen Bereichen eines Flughafens mithilfe eines ebensolchen Chord-Diagrammes aufgezeigt werden. Da Chord-Diagramme oft sehr viele Daten darstellen, haben sie die Tendenz, in der Gesamtansicht schnell unübersichtlich zu werden. Deshalb soll das Diagramm interaktiv sein, damit einzelne Personenflüsse hervorgehoben werden können, oder selektiv nur ein Teil der Daten angezeigt werden.
Hier das Endresultat, dessen Erstellung in diesem zweiteiligen Blogpost erläutert wird:
Klick zum Vergrössern – die Farbqualität ist reduziert, da GIF-Datei.
2. Chord-Diagramm mit Standardeinstellungen
2.1. Programmierumgebung und Framework
Am einfachsten gestaltet sich die Entwicklung von D3.js-Diagrammen in einer statischen HTML-Webseite ohne jeglichen Overhead, sofern ein Satz an plausiblen Beispieldaten als JavaScript-Objekte exportiert werden kann. Danach kann der JavaScript-Code einfach in eine beliebige andere Web-Umgebung eingebunden werden. Entsprechend starte ich hier mit einer einfachen HTML-Datei, setze einen dunklen Hintergrund mit CSS, und importiere das D3.js-Framework. Für dieses Beispiel wird D3.js Version 3 verwendet. Die verschiedenen Versionen von D3.js sind in der Regel nicht rückwärtskompatibel, jedoch kann es je nach Anforderungen und/oder bereits erarbeiteter Kniffe und Tricks einfacher sein, mit älteren Versionen zu arbeiten.
<html> <head> <meta content="text/html;charset=utf-8" /> <script src="https://d3js.org/d3.v3.min.js"></script> <style> body { background-color: #111111; } </style> </head> <body> <div id="body"> <div id="chart"></div> </div> <script type="text/javascript"> //D3.js / JavaScript code goes here </script> </body> </html>
2.2. Datenaufbereitung
Um das Diagramm zu entwickeln, wurden in Excel einige Testdaten erstellt, welche zwar erfunden sind, aber dennoch plausibel sein könnten. Diese Daten wurden dann so exportiert und umformatiert, dass sie als gültiger JavaScript-Code vorliegen. Die Daten bestehen aus zwei Arrays:
- Liste aller Bereiche des Flughafens. Jeder Bereich erhält eine ID, einen Namen und eine Farbe, mit welcher der entsprechende Bereich im Diagramm stilisiert werden soll.
- Liste aller Personenflüsse. Für jeden Eintrag wird je eine ID für Start und Ziel, sowie die Quantität angegeben. Der Eintrag “from: 11, to: 6, quantity: 221” würde also bedeuten, dass 221 Personen vom Bereich mit ID 11 zum Bereich mit ID 6 reisten.
var locations = [ { id: 0, name: "Gate A", color: "#12B32D" }, { id: 1, name: "Gate B", color: "#0D8020" }, { id: 2, name: "Gate D", color: "#095916" }, { id: 3, name: "Gate E", color: "#064010" }, { id: 4, name: "Check-in 1", color: "#F4CF11" }, { id: 5, name: "Check-in 2", color: "#B3970C" }, { id: 6, name: "Check-in 3", color: "#665607" }, { id: 7, name: "Airside Center", color: "#0D6180" }, { id: 8, name: "Airport Shopping", color: "#16A2D5" }, { id: 9, name: "P1", color: "#01FAF1" }, { id: 10, name: "P2", color: "#14CCCC" }, { id: 11, name: "P3", color: "#0F9999" }, { id: 12, name: "P4", color: "#0C8080" }, { id: 13, name: "P5", color: "#074D4D" }, { id: 14, name: "SBB", color: "#F27900" }, { id: 15, name: "Bus/Tram", color: "#EF4F00" } ]; var flows = [ { from: 0, to: 0, quantity: 428 }, { from: 0, to: 1, quantity: 5 }, { from: 0, to: 2, quantity: 2 }, { from: 0, to: 3, quantity: 10 }, { from: 0, to: 4, quantity: 1 }, { from: 0, to: 5, quantity: 8 }, { from: 0, to: 6, quantity: 0 }, { from: 0, to: 7, quantity: 86 }, { from: 0, to: 8, quantity: 318 }, { from: 0, to: 9, quantity: 30 }, { from: 0, to: 10, quantity: 23 }, { from: 0, to: 11, quantity: 67 }, { from: 0, to: 12, quantity: 101 }, { from: 0, to: 13, quantity: 10 }, { from: 0, to: 14, quantity: 270 }, { from: 0, to: 15, quantity: 120 }, { from: 1, to: 0, quantity: 0 }, { from: 1, to: 1, quantity: 128 }, { from: 1, to: 2, quantity: 40 }, { from: 1, to: 3, quantity: 10 }, { from: 1, to: 4, quantity: 0 }, { from: 1, to: 5, quantity: 30 }, { from: 1, to: 6, quantity: 10 }, { from: 1, to: 7, quantity: 78 }, { from: 1, to: 8, quantity: 172 }, { from: 1, to: 9, quantity: 90 }, { from: 1, to: 10, quantity: 2 }, { from: 1, to: 11, quantity: 10 }, { from: 1, to: 12, quantity: 13 }, { from: 1, to: 13, quantity: 56 }, { from: 1, to: 14, quantity: 134 }, { from: 1, to: 15, quantity: 87 }, { from: 2, to: 0, quantity: 0 }, { from: 2, to: 1, quantity: 3 }, { from: 2, to: 2, quantity: 97 }, { from: 2, to: 3, quantity: 7 }, { from: 2, to: 4, quantity: 12 }, { from: 2, to: 5, quantity: 9 }, { from: 2, to: 6, quantity: 3 }, { from: 2, to: 7, quantity: 11 }, { from: 2, to: 8, quantity: 109 }, { from: 2, to: 9, quantity: 2 }, { from: 2, to: 10, quantity: 3 }, { from: 2, to: 11, quantity: 12 }, { from: 2, to: 12, quantity: 9 }, { from: 2, to: 13, quantity: 0 }, { from: 2, to: 14, quantity: 76 }, { from: 2, to: 15, quantity: 26 }, { from: 3, to: 0, quantity: 3 }, { from: 3, to: 1, quantity: 10 }, { from: 3, to: 2, quantity: 9 }, { from: 3, to: 3, quantity: 390 }, { from: 3, to: 4, quantity: 0 }, { from: 3, to: 5, quantity: 0 }, { from: 3, to: 6, quantity: 12 }, { from: 3, to: 7, quantity: 43 }, { from: 3, to: 8, quantity: 126 }, { from: 3, to: 9, quantity: 207 }, { from: 3, to: 10, quantity: 23 }, { from: 3, to: 11, quantity: 10 }, { from: 3, to: 12, quantity: 36 }, { from: 3, to: 13, quantity: 78 }, { from: 3, to: 14, quantity: 532 }, { from: 3, to: 15, quantity: 265 }, { from: 4, to: 0, quantity: 165 }, { from: 4, to: 1, quantity: 277 }, { from: 4, to: 2, quantity: 80 }, { from: 4, to: 3, quantity: 109 }, { from: 4, to: 4, quantity: 78 }, { from: 4, to: 5, quantity: 34 }, { from: 4, to: 6, quantity: 10 }, { from: 4, to: 7, quantity: 23 }, { from: 4, to: 8, quantity: 381 }, { from: 4, to: 9, quantity: 40 }, { from: 4, to: 10, quantity: 35 }, { from: 4, to: 11, quantity: 21 }, { from: 4, to: 12, quantity: 54 }, { from: 4, to: 13, quantity: 3 }, { from: 4, to: 14, quantity: 38 }, { from: 4, to: 15, quantity: 38 }, { from: 5, to: 0, quantity: 80 }, { from: 5, to: 1, quantity: 12 }, { from: 5, to: 2, quantity: 5 }, { from: 5, to: 3, quantity: 254 }, { from: 5, to: 4, quantity: 10 }, { from: 5, to: 5, quantity: 97 }, { from: 5, to: 6, quantity: 22 }, { from: 5, to: 7, quantity: 35 }, { from: 5, to: 8, quantity: 103 }, { from: 5, to: 9, quantity: 67 }, { from: 5, to: 10, quantity: 12 }, { from: 5, to: 11, quantity: 0 }, { from: 5, to: 12, quantity: 6 }, { from: 5, to: 13, quantity: 2 }, { from: 5, to: 14, quantity: 10 }, { from: 5, to: 15, quantity: 8 }, { from: 6, to: 0, quantity: 12 }, { from: 6, to: 1, quantity: 220 }, { from: 6, to: 2, quantity: 70 }, { from: 6, to: 3, quantity: 0 }, { from: 6, to: 4, quantity: 12 }, { from: 6, to: 5, quantity: 8 }, { from: 6, to: 6, quantity: 238 }, { from: 6, to: 7, quantity: 12 }, { from: 6, to: 8, quantity: 3 }, { from: 6, to: 9, quantity: 30 }, { from: 6, to: 10, quantity: 10 }, { from: 6, to: 11, quantity: 38 }, { from: 6, to: 12, quantity: 8 }, { from: 6, to: 13, quantity: 12 }, { from: 6, to: 14, quantity: 20 }, { from: 6, to: 15, quantity: 7 }, { from: 7, to: 0, quantity: 87 }, { from: 7, to: 1, quantity: 20 }, { from: 7, to: 2, quantity: 123 }, { from: 7, to: 3, quantity: 143 }, { from: 7, to: 4, quantity: 9 }, { from: 7, to: 5, quantity: 2 }, { from: 7, to: 6, quantity: 0 }, { from: 7, to: 7, quantity: 457 }, { from: 7, to: 8, quantity: 30 }, { from: 7, to: 9, quantity: 10 }, { from: 7, to: 10, quantity: 32 }, { from: 7, to: 11, quantity: 19 }, { from: 7, to: 12, quantity: 3 }, { from: 7, to: 13, quantity: 4 }, { from: 7, to: 14, quantity: 73 }, { from: 7, to: 15, quantity: 25 }, { from: 8, to: 0, quantity: 120 }, { from: 8, to: 1, quantity: 38 }, { from: 8, to: 2, quantity: 96 }, { from: 8, to: 3, quantity: 167 }, { from: 8, to: 4, quantity: 3 }, { from: 8, to: 5, quantity: 23 }, { from: 8, to: 6, quantity: 9 }, { from: 8, to: 7, quantity: 47 }, { from: 8, to: 8, quantity: 97 }, { from: 8, to: 9, quantity: 123 }, { from: 8, to: 10, quantity: 86 }, { from: 8, to: 11, quantity: 90 }, { from: 8, to: 12, quantity: 34 }, { from: 8, to: 13, quantity: 12 }, { from: 8, to: 14, quantity: 176 }, { from: 8, to: 15, quantity: 192 }, { from: 9, to: 0, quantity: 30 }, { from: 9, to: 1, quantity: 87 }, { from: 9, to: 2, quantity: 9 }, { from: 9, to: 3, quantity: 123 }, { from: 9, to: 4, quantity: 376 }, { from: 9, to: 5, quantity: 233 }, { from: 9, to: 6, quantity: 199 }, { from: 9, to: 7, quantity: 43 }, { from: 9, to: 8, quantity: 90 }, { from: 9, to: 9, quantity: 0 }, { from: 9, to: 10, quantity: 0 }, { from: 9, to: 11, quantity: 0 }, { from: 9, to: 12, quantity: 4 }, { from: 9, to: 13, quantity: 0 }, { from: 9, to: 14, quantity: 10 }, { from: 9, to: 15, quantity: 2 }, { from: 10, to: 0, quantity: 23 }, { from: 10, to: 1, quantity: 1 }, { from: 10, to: 2, quantity: 9 }, { from: 10, to: 3, quantity: 6 }, { from: 10, to: 4, quantity: 197 }, { from: 10, to: 5, quantity: 201 }, { from: 10, to: 6, quantity: 66 }, { from: 10, to: 7, quantity: 7 }, { from: 10, to: 8, quantity: 143 }, { from: 10, to: 9, quantity: 2 }, { from: 10, to: 10, quantity: 0 }, { from: 10, to: 11, quantity: 0 }, { from: 10, to: 12, quantity: 1 }, { from: 10, to: 13, quantity: 0 }, { from: 10, to: 14, quantity: 2 }, { from: 10, to: 15, quantity: 18 }, { from: 11, to: 0, quantity: 0 }, { from: 11, to: 1, quantity: 2 }, { from: 11, to: 2, quantity: 0 }, { from: 11, to: 3, quantity: 4 }, { from: 11, to: 4, quantity: 67 }, { from: 11, to: 5, quantity: 23 }, { from: 11, to: 6, quantity: 221 }, { from: 11, to: 7, quantity: 12 }, { from: 11, to: 8, quantity: 4 }, { from: 11, to: 9, quantity: 10 }, { from: 11, to: 10, quantity: 0 }, { from: 11, to: 11, quantity: 0 }, { from: 11, to: 12, quantity: 0 }, { from: 11, to: 13, quantity: 0 }, { from: 11, to: 14, quantity: 3 }, { from: 11, to: 15, quantity: 0 }, { from: 12, to: 0, quantity: 2 }, { from: 12, to: 1, quantity: 16 }, { from: 12, to: 2, quantity: 10 }, { from: 12, to: 3, quantity: 8 }, { from: 12, to: 4, quantity: 412 }, { from: 12, to: 5, quantity: 321 }, { from: 12, to: 6, quantity: 100 }, { from: 12, to: 7, quantity: 54 }, { from: 12, to: 8, quantity: 89 }, { from: 12, to: 9, quantity: 0 }, { from: 12, to: 10, quantity: 2 }, { from: 12, to: 11, quantity: 4 }, { from: 12, to: 12, quantity: 0 }, { from: 12, to: 13, quantity: 0 }, { from: 12, to: 14, quantity: 0 }, { from: 12, to: 15, quantity: 0 }, { from: 13, to: 0, quantity: 0 }, { from: 13, to: 1, quantity: 3 }, { from: 13, to: 2, quantity: 30 }, { from: 13, to: 3, quantity: 2 }, { from: 13, to: 4, quantity: 80 }, { from: 13, to: 5, quantity: 83 }, { from: 13, to: 6, quantity: 20 }, { from: 13, to: 7, quantity: 10 }, { from: 13, to: 8, quantity: 0 }, { from: 13, to: 9, quantity: 0 }, { from: 13, to: 10, quantity: 0 }, { from: 13, to: 11, quantity: 0 }, { from: 13, to: 12, quantity: 1 }, { from: 13, to: 13, quantity: 4 }, { from: 13, to: 14, quantity: 10 }, { from: 13, to: 15, quantity: 32 }, { from: 14, to: 0, quantity: 30 }, { from: 14, to: 1, quantity: 45 }, { from: 14, to: 2, quantity: 10 }, { from: 14, to: 3, quantity: 2 }, { from: 14, to: 4, quantity: 486 }, { from: 14, to: 5, quantity: 512 }, { from: 14, to: 6, quantity: 89 }, { from: 14, to: 7, quantity: 10 }, { from: 14, to: 8, quantity: 188 }, { from: 14, to: 9, quantity: 12 }, { from: 14, to: 10, quantity: 8 }, { from: 14, to: 11, quantity: 0 }, { from: 14, to: 12, quantity: 4 }, { from: 14, to: 13, quantity: 22 }, { from: 14, to: 14, quantity: 12 }, { from: 14, to: 15, quantity: 287 }, { from: 15, to: 0, quantity: 30 }, { from: 15, to: 1, quantity: 2 }, { from: 15, to: 2, quantity: 8 }, { from: 15, to: 3, quantity: 0 }, { from: 15, to: 4, quantity: 275 }, { from: 15, to: 5, quantity: 100 }, { from: 15, to: 6, quantity: 45 }, { from: 15, to: 7, quantity: 8 }, { from: 15, to: 8, quantity: 87 }, { from: 15, to: 9, quantity: 2 }, { from: 15, to: 10, quantity: 0 }, { from: 15, to: 11, quantity: 0 }, { from: 15, to: 12, quantity: 8 }, { from: 15, to: 13, quantity: 2 }, { from: 15, to: 14, quantity: 310 }, { from: 15, to: 15, quantity: 54 } ];
Der Grossteil der geometrischen Arbeit wird D3.js für uns übernehmen. Damit dies jedoch klappt, müssen die Daten im korrekten Format vorliegen. In diesem Fall erwartet D3.js die Personenfluss-Daten als quadratische Matrix, wobei die Zeilen für die Ziele der Flüsse und die Spalten für die Ursprünge der Flüsse stehen.
var matrix = []; //Map list of data to matrix flows.forEach(function (flow) { //Initialize sub-array if not yet exists if (!matrix[flow.to]) { matrix[flow.to] = []; } matrix[flow.to][flow.from] = flow.quantity; });
2.3. Chord-Diagramm
Nun sind die Daten bereit, und das Chord-Diagramm kann erstellt werden. In einem ersten Schritt verzichten wir auf möglichst allen “Schnickschnack”, und programmieren nur gerade das Nötigste.
2.3.1. Grösse und Position
Zuerst muss definiert werden, wie gross das Diagramm sein soll. Hier wird mit einer fixen Grösse gearbeitet. Zudem wird ein wenig Platz um das Diagramm bereits mit einer margin festgehalten, damit später Platz bleibt für Beschriftungen. Die beiden wichtigsten Grössen des Diagramms sind der innere und äussere Radius. Der innere Radius entspricht dem Innenradius der Ringsegmente aussen am Diagramm, und auch dem Radius, in welchem sich die Verbindungen (“Chords”) befinden. Die Radien wurden relativ zur Gesamtgrösse definiert, damit bei einer Anpassung der Gesamtgrösse auch der Rest des Diagramms skaliert wird.
Danach werden einige Container-Elemente definiert und konfiguriert. Diese werden ähnlich verwendet wie <div>-Elemente bei HTML. Dabei werden diese Container mittels dem Befehl d3.select() selektiert, welcher ein Selection-Objekt retourniert, das danach mittels fluent syntax konfiguriert werden kann. Mit .append() können Child-Elemente (HTML oder SVG-Elemente) angefügt werden; mit .attr() können jegliche HTML/SVG-Attribute bearbeitet werden.
Das Element “container” wird ins Zentrum des Diagramms verschoben (mit CSS Transform-Translate). Alle Diagrammelemente, welche als Child Elements in diesen Container eingefügt werden haben damit automatisch das Zentrum als Ursprung und können so einfacher platziert werden.
var size = 1300; var margin = { top: 0, right: 70, bottom: 70, left: 70 }; var width = size - margin.left - margin.right; var height = size - margin.top - margin.bottom; var innerRadius = Math.min(width, height) * .39; var outerRadius = innerRadius * 1.08; var root = d3.select("#chart"); var svg = root.append("svg:svg") .attr("width", width + margin.left + margin.right) .attr("height", height + margin.top + margin.bottom); var container = svg.append("g") .attr("transform", "translate(" + (margin.left + width / 2) + "," + (margin.top + height / 2) + ")");
2.3.2. D3-Generatoren
Nun kommt D3.js zum Zug: Wir fordern nun das Framework dazu auf, anhand der oben generierten Datenmatrix die entsprechenden SVG-Elemente zu generieren. Dazu nutzt D3 sogenannte Generatoren. Diese existieren für verschiedene Formen, unter anderem eben auch für die Elemente von Chord-Diagrammen. Mit folgendem Code wird ein solcher Generator initialisiert – gebraucht wird er in Kürze.
var chord = d3.layout.chord() .matrix(matrix);
Einen weiteren Generator benötigen wir für die Ring-Segmente, welche im D3.js “Arc” genannt werden. Hier werden als Konfiguration die beiden Begrenzungsradien benötigt.
var arc = d3.svg.arc() .innerRadius(innerRadius) .outerRadius(outerRadius);
2.3.3. Zeichnen der Kreissegmente
Jetzt wird zum ersten mal wirklich etwas gezeichnet, und zwar die Kreissegmente. Dafür benötigen wir einerseits den Kreissegment-Generator, aber auch den Chord-Generator. Denn letzterer berechnet, wo die jeweiligen Kreissegmente hinkommen sollen, und liefert dafür für jedes Kreissegment einen Start- und Endwinkel. Da für jedes Kreissegment später noch zusätzliche Dinge, wie etwa die Beschriftung, hinzukommen, packen wir jedes Segment in eine eigene Gruppe (<g>) – das SVG-Äquivalent zum HTML-<div>.
Das .selectAll(…).data(…).enter().append()-Pattern ist typisch für D3.js, und anfangs etwas knifflig zu verstehen. Die Idee dabei ist, dass für jedes Element einer Liste von Daten ein neues Element (hier “g”) generiert wird. Die Daten werden hier vom Chord-Generator geliefert (chord.groups). Für jeden Eintrag, für welchen es noch kein Element gibt (enter()) wird ein neues Element erstellt (append()) und der entsprechende Datensatz mit diesem Element assoziiert (data()). Die Variable “g” enthält nachher eine D3-Selection über alle Elemente (selectAll()). Der nachfolgende Aufruf g.append(“svg:path”) bedeutet dann, dass an jedes Element ein neuer SVG-Pfad angehängt wird.
SVG-Pfad-Elemente haben ein Attribut namens “d”. In dieses Attribut wird in einer speziellen Syntax definiert, was für eine Form das Element haben soll. Diese Zeichenfolge wird dabei durch einen D3-Generator erstellt, indem der entsprechende Generator spezifiziert wird. In diesem Fall ist dies die Variable “arc”, welche den oben erstellten Kreissegment-Generator enthält.
Damit die Kreissegmente visuell unterscheidbar sind, geben wir ihnen eine Farbe. Die entsprechende Farbe erhalten wir aus der Liste aller Bereiche (siehe oben). Den korrekten Listenindex bekommen wir vom Datenelement, welches mit dem data()-Befehl an das jeweilige Element gebunden wurde. Jedem Kreissegment geben wir so eine Füllfarbe. Die Umrandung (“stroke”) färben wir ebenfalls, und zwar in einer leicht helleren Farbe als die Füllung. Dazu kann d3.rgb(…).brighter() verwendet werden.
var g = container.selectAll("g.group") .data(chord.groups) .enter() .append("svg:g") .attr("class", "group"); g.append("svg:path") .attr("d", arc) .style("fill", function (d) { return locations[d.index].color; }) .style("stroke", function (d) { return d3.rgb(locations[d.index].color).brighter(); });
2.3.4. Zeichnen der Chords
Schliesslich werden die Chords gezeichnet, welche ähnlich funktionieren wie die Kreissegmente. Die Form übernehmen D3-Generatoren, die Farben setzen wir selbstständig.
var chords = container.selectAll("path.chord") .data(chord.chords) .enter() .append("svg:path") .attr("class", "chord") .attr("d", d3.svg.chord().radius(innerRadius)) .style("fill", function (d) { return locations[d.source.index].color; }) .style("stroke", function (d) { return d3.rgb(locations[d.source.index].color); }) .style("fill-opacity", "0.7");
Und damit ist ein erster Meilenstein erreicht! Das Diagramm ist da, es ist schön farbig, aber gut lesbar und übersichtlich ist anders!
HTML-Datei: basic_chord_diagram
3. Beschriftungen
Am wichtigsten ist bei diesem Stand des Diagrammes offensichtlich, einmal zu deklarieren, welche Farbe was bedeutet. Zudem wäre eine Angabe der Quantitäten hilfreich – man sieht zwar bereits, von wo nach wo viele und wo wenige Leute unterwegs sind, aber man hat noch keinen Anhaltspunkt, um welche Grössenordnung es geht, oder welche Farbe für welchen Bereich steht.
3.1. Ticks und Anzahl Personen
Für die Angabe der Quantitäten soll entlang der Ringsegmente pro 100 Personen ein Tick, also ein kleiner Strich, angezeigt werden. Zudem soll pro 500 Personen eine Beschriftung angefügt werden.
Zuerst muss für jeden Tick der Rotationswinkel berechnet werden. Dies machen wir mit einer kleinen Hilfsfunktion. Als Argument (d) erhält die Funktion ein Gruppenelement des Chord-Generators, welches Start- und Endwinkel, sowie die Anzahl Personen für das Kreissegment enthält. Mit d3.range(0, d.value, 100) erhalten wir eine Sequenz von Werten mit Schrittweite 100, von 0 bis zur Anzahl Personen im Ringsegment. Basierend darauf werden die Winkel der Ticks berechnet und in einem Objekt gespeichert. Dieses Objekt enthält neben dem Tick-Winkel auch gerade die Beschriftung des Ticks, welche nur bei jedem fünften Tick verschieden von Null ist.
function groupTicks(d) { var anglePerPerson = (d.endAngle - d.startAngle) / d.value; return d3.range(0, d.value, 100).map(function (v, i) { return { angle: v * anglePerPerson + d.startAngle, label: i % 5 ? null : v //Each 5th tick has a label }; }); };
Nachfolgend werden die Ticks gezeichnet und die Beschriftungen platziert. Der grösste Teil dieses Codes wird ausschliesslich für das Styling (Farbe, Schriftart, Text links- oder rechtsbündig usw.) benötigt. Für die Positionierung der Ticks und Beschriftungen wird mit dem CSS Transform-Attribut gearbeitet.
var ticks = g.append("svg:g") .selectAll("g.ticks") .data(groupTicks) .enter().append("svg:g") .attr("class", "ticks") .attr("transform", function (d) { return "rotate(" + (d.angle * 180 / Math.PI - 90) + ")" + "translate(" + outerRadius + 40 + ",0)"; }); /*Append the tick around the arcs*/ ticks.append("svg:line") .attr("x1", 1) .attr("y1", 0) .attr("x2", 6) .attr("y2", 0) .attr("class", "ticks") .style("stroke", "#FFF") .style("stroke-width", "1.5px"); /*Add the labels for the ticks*/ ticks.append("svg:text") .attr("class", "tickLabels") .attr("x", 12) .attr("dy", ".35em") .style("font-size", "10px") .style("font-family", "sans-serif") .attr("fill", "#FFF") .attr("transform", function (d) { return d.angle > Math.PI ? "rotate(180)translate(-25)" : null; }) .style("text-anchor", function (d) { return d.angle > Math.PI ? "end" : null; }) .text(function (d) { return d.label; });
3.2. Bereichsnamen
Auf eine genauere Erklärung dieses Teiles verzichte ich an dieser Stelle – die meisten Überlegungen sind rein geometrischer Natur.
var dr = 40; //radial translation for group names var dx = 20; //horizontal translation for group names g.append("svg:text") .each(function (d) { d.angle = (d.startAngle + d.endAngle) / 2; }) .attr("dy", ".35em") .attr("class", "titles") .style("font-size", "14px") .style("font-family", "sans-serif") .attr("fill", "#FFF") .attr("text-anchor", function (d) { return d.angle > Math.PI ? "end" : null; }) .attr("transform", function (d) { var r = outerRadius + dr; var angle = d.angle + ((3 *Math.PI) / 2); var x = r * Math.cos(angle); var y = r * Math.sin(angle); if (d.angle > Math.PI) { x -= dx; } else { x += dx; } return "translate(" + x + ", " + y + ")"; }) .text(function (d, i) { return locations[i].name; }); /*Lines from labels to arcs*/ /*part in radial direction*/ this.g.append("line") .attr("x1", function (d) { return outerRadius * Math.cos(d.angle + ((3 * Math.PI) / 2)); }) .attr("y1", function (d) { return outerRadius * Math.sin(d.angle + ((3 * Math.PI) / 2)); }) .attr("x2", function (d) { return (outerRadius + dr) * Math.cos(d.angle + ((3 * Math.PI) / 2)); }) .attr("y2", function (d) { return (outerRadius + dr) * Math.sin(d.angle + ((3 * Math.PI) / 2)); }) .attr("class", "ticks") .style("stroke", "#FFF") .style("stroke-width", "0.5px"); /*horizontal part*/ this.g.append("line") .attr("x1", function (d) { return (outerRadius + dr) * Math.cos(d.angle + ((3 * Math.PI) / 2)); }) .attr("y1", function (d) { return (outerRadius + dr) * Math.sin(d.angle + ((3 * Math.PI) / 2)); }) .attr("x2", function (d) { var x = (outerRadius + dr) * Math.cos(d.angle + ((3 * Math.PI) / 2)); if (d.angle > Math.PI) { x -= dx - 5; } else { x += dx - 5; } return x; }) .attr("y2", function (d) { return (outerRadius + dr) * Math.sin(d.angle + ((3 * Math.PI) / 2)); }) .attr("class", "ticks") .style("stroke", "#FFF") .style("stroke-width", "0.5px");
Damit sieht das Diagramm nun so aus:
4. Chord-Sortierung und Group-spacing
Als Abschluss des ersten Teiles sollen die Chords noch sortiert werden, und das Diagramm etwas mehr Luft erhalten, indem zwischen den Bereichen ein kleiner Abstand eingefügt wird. Beides macht uns D3.js sehr einfach. Dazu wird der Teil des Codes aus dem zweiten Schritt, wo die Daten dem Chord-Generator übergeben wurden, wie folgt erweitert:
var chord = d3.layout.chord() .padding(0.04) .sortSubgroups(d3.descending) /*sort the chords inside an arc from high to low*/ .sortChords(d3.ascending) /*which chord should be shown on top when chords cross. Now the largest chord is at the top*/ .matrix(matrix);
Mit folgendem Resultat:
HTML-Datei: basic_chord_diagram_labelled
Im nächsten Teil wird es dann darum gehen, die Chords noch besser zu sortieren, damit sich möglichst wenige davon überkreuzen. Zudem wird dort noch gezeigt, wie die Formen der Chords angepasst werden können, um die Überlappungen weiter zu reduzieren. Schlussendlich wird das Diagramm noch um Interaktivität ergänzt.
Teil 2: https://blog.noser.com/d3-js-chord-diagramm-teil-2-benutzerdefinierte-sortierung-und-kurvenformen/
Pingback: Noser Blog D3.js: Chord-Diagramm Teil 2 - Benutzerdefinierte Sortierung und Kurvenformen - Noser Blog