Erstellen eines Gantt-ähnlichen Diagramms mithilfe von D3 zur Visualisierung eines Datasets

Wenn Sie mit den Grundlagen von D3.js fertig sind, besteht der nächste Schritt in der Regel darin, Visualisierungen mit Ihrem Datensatz zu erstellen. Aufgrund der Funktionsweise von D3 kann die Art und Weise, wie wir den Datensatz organisieren, unser Leben sehr einfach oder sehr schwer machen.

In diesem Artikel werden wir verschiedene Aspekte dieses Bauprozesses diskutieren. Um diese Aspekte zu veranschaulichen, erstellen wir eine Visualisierung, die einem Gantt-Diagramm ähnelt.

Die wichtigste Lektion, die ich gelernt habe, ist, dass Sie ein Dataset erstellen müssen, bei dem jeder Datenpunkt einer Dateneinheit Ihres Diagramms entspricht. Sehen wir uns unsere Fallstudie an, um zu sehen, wie dies funktioniert.

Ziel ist es, ein Gantt-ähnliches Diagramm zu erstellen, das dem folgenden ähnelt:

Die Visualisierung, die wir erstellen möchten

Wie Sie sehen, handelt es sich nicht um ein Gantt-Diagramm, da die Aufgaben am selben Tag beginnen und enden.

Datensatz erstellen

Ich habe die Daten aus Minuten extrahiert. Für jede Textdatei erhielt ich Informationen zu den Projekten und deren Status aus Besprechungen. Zuerst habe ich meine Daten so strukturiert:

{
    "Sitzungen": [{
            "label": "1st Meeting",
            "Datum": "09/03/2017",
            "projects_presented": [],
            "projects_approved": ["002/2017"],
            "projects_voting_round_1": ["005/2017"],
            "projects_voting_round_2": ["003/2017", "004/2017"]
        },
        {
            "label": "2nd Meeting",
            "date_start": "10/03/2017",
            "projects_presented": ["006/2017"],
            "projects_approved": ["003/2017", "004/2017"],
            "projects_voting_round_1": [],
            "projects_voting_round_2": ["005/2017"]
        }
    ]
}

Schauen wir uns die Daten genauer an.

Jedes Projekt hat 4 Status: Präsentiert, Abstimmungsrunde 1, Abstimmungsrunde 2 und genehmigt. In jeder Besprechung kann sich der Status für die Projekte ändern oder nicht. Ich habe die Daten strukturiert, indem ich sie nach Besprechungen gruppiert habe. Diese Gruppierung bereitete uns beim Erstellen der Visualisierung viele Probleme. Dies lag daran, dass wir Daten mit D3 an Knoten übergeben mussten. Nachdem ich das Gantt-Diagramm gesehen hatte, das Jess Peter hier erstellt hatte, wurde mir klar, dass ich meine Daten ändern musste.

Was waren die Mindestinformationen, die ich anzeigen wollte? Was war der Mindestknoten? Betrachtet man das Bild, so handelt es sich um die Informationen des Projekts. Deshalb habe ich die Struktur der Daten folgendermaßen geändert:

{
  "projekte": [
                  {
                    "meeting": "1st Meeting",
                    "Typ": "Projekt",
                    "Datum": "09/03/2017",
                    "label": "Projekt 002/2017",
                    "Status": "genehmigt"
                  },
                  {
                    "meeting": "1st Meeting",
                    "Typ": "Projekt",
                    "Datum": "09/03/2017",
                    "label": "Projekt 005/2017",
                    "status": "voting_round_1"
                  },
                  {
                    "meeting": "1st Meeting",
                    "Typ": "Projekt",
                    "Datum": "09/03/2017",
                    "label": "Projekt 003/2017",
                    "status": "voting_round_2"
                  },
                  {
                    "meeting": "1st Meeting",
                    "Typ": "Projekt",
                    "Datum": "09/03/2017",
                    "label": "Projekt 004/2017",
                    "status": "voting_round_2"
                  }
               ]
}

Und danach hat alles besser geklappt. Es ist lustig, wie die Frustration nach dieser einfachen Änderung verschwunden ist.

Visualisierung erstellen

Nachdem wir den Datensatz haben, beginnen wir mit der Erstellung der Visualisierung.

Erstellen der X-Achse

Jedes Datum sollte auf der x-Achse angezeigt werden. Dazu definieren Sie d3.timeScale ():

var timeScale = d3.scaleTime ()
                .domain (d3.extent (datensatz, d => dateFormat (d.date)))
                Bereich ([0, 500]);

Die minimalen und maximalen Werte sind in arrayd3.extent () angegeben.

Jetzt, da Sie timeScale haben, können Sie die Achse aufrufen.

var xAxis = d3.axisBottom ()
                .scale (Zeitskala)
                .ticks (d3.timeMonth)
                .tickSize (250, 0, 0)
                .tickSizeOuter (0);

Die Zecken sollten 250px lang sein. Sie wollen das äußere Häkchen nicht. Der Code zum Anzeigen der Achse lautet:

d3.json ("projects.json", Funktion (Fehler, Daten) {
            Diagramm (data.projects);
});
Funktionsplan (Daten) {
    var dateFormat = d3.timeParse ("% d /% m /% Y");
    var timeScale = d3.scaleTime ()
                   .domain (d3.extent (data, d => dateFormat (d.date)))
                   Bereich ([0, 500]);
    var xAxis = d3.axisBottom ()
                  .scale (Zeitskala)
                  .tickSize (250, 0, 0)
                  .tickSizeOuter (0);
    var grid = d3.select ("svg"). append ('g'). call (xAxis);
}

Wenn Sie dies zeichnen, können Sie sehen, dass es viele Häkchen gibt. Tatsächlich gibt es Zecken für jeden Tag des Monats. Wir möchten nur die Tage anzeigen, an denen Besprechungen stattgefunden haben. Dazu setzen wir die Tick-Werte explizit:

let dataByDates = d3.nest (). key (d => d.date) .entries (data);
let tickValues ​​= dataByDates.map (d => dateFormat (d.key));
var xAxis = d3.axisBottom ()
                .scale (Zeitskala)
                .tickValues ​​(tickValues)
                .tickSize (250, 0, 0)
                .tickSizeOuter (0);

Mit d3.nest () können Sie alle Projekte nach Datum gruppieren (wie praktisch ist es, die Daten nach Projekten zu strukturieren?), Dann alle Daten abrufen und an die Achse übergeben.

Projekte platzieren

Wir müssen die Projekte entlang der y-Achse platzieren. Definieren wir also einen neuen Maßstab:

yScale = d3.scaleLinear (). domain ([0, data.length]). range ([0, 250]);

Die Domain ist die Anzahl der Projekte. Der Bereich ist die Größe jedes Ticks. Jetzt können wir die Rechtecke platzieren:

var projects = d3.select ("svg")
                   .append ('g')
                   .selectAll ("this_is_empty")
                   .data (Daten)
                   .eingeben();
var innerRects = projects.append ("rect")
              .attr ("rx", 3)
              .attr ("ry", 3)
              .attr ("x", (d, i) => timeScale (dateFormat (d.date)))
              .attr ("y", (d, i) => yScale (i))
              .attr ("width", 200)
              .attr ("Höhe", 30)
              .attr ("Strich", "keine")
              .attr ("füllen", "hellblau");

selectAll (), data (), enter () und append () werden immer kniffliger. Um die enter () -Methode zu verwenden (um einen neuen Knoten aus einem Datenpunkt zu erstellen), benötigen wir eine Auswahl. Deshalb benötigen wir selectAll ("this_is_empty"), auch wenn wir noch keine richtigen haben. Ich habe diesen Namen verwendet, um zu verdeutlichen, dass wir nur die leere Auswahl benötigen. Mit anderen Worten, wir verwenden selectAll ("this_is_empty"), um eine leere Auswahl zu erhalten, an der wir arbeiten können.

In den variablen Projekten sind leere Auswahlen an Daten gebunden, sodass wir sie zum Zeichnen der Projekte in innerRects verwenden können.

Jetzt können Sie auch für jedes Projekt eine Bezeichnung hinzufügen:

var rectText = projects.append ("text")
                .text (d => d.label)
                .attr ("x", d => timeScale (dateFormat (d.date)) + 100)
                .attr ("y", (d, i) => yScale (i) + 20)
                .attr ("Schriftgröße", 11)
                .attr ("text-anchor", "middle")
                .attr ("Texthöhe", 30)
                .attr ("fill", "#fff");

Färben Sie jedes Projekt

Wir möchten, dass die Farbe jedes Rechtecks ​​den Status jedes Projekts widerspiegelt. Dazu erstellen wir eine weitere Skala:

let dataByCategories = d3.nest (). key (d => d.status) .entries (data);
let categories = dataByCategories.map (d => d.key) .sort ();
let colorScale = d3.scaleLinear ()
             .domain ([0, categories.length])
             .range (["# 00B9FA", "# F95002"])
             .interpolate (d3.interpolateHcl);

Und dann können wir die Rechtecke mit Farben dieser Skala füllen. Hier ist der Code, der alles zusammenfasst, was wir bisher gesehen haben:

d3.json ("projects.json", Funktion (Fehler, Daten) {
            Diagramm (data.projetos);
        });
Funktionsplan (Daten) {
    var dateFormat = d3.timeParse ("% d /% m /% Y");
    var timeScale = d3.scaleTime ()
                   .domain (d3.extent (data, d => dateFormat (d.date)))
                   Bereich ([0, 500]);
  
    let dataByDates = d3.nest (). key (d => d.date) .entries (data);
    let tickValues ​​= dataByDates.map (d => dateFormat (d.key));
  
    let dataByCategories = d3.nest (). key (d => d.status) .entries (data);
    let categories = dataByCategories.map (d => d.key) .sort ();
    let colorScale = d3.scaleLinear ()
                 .domain ([0, categories.length])
                 .range (["# 00B9FA", "# F95002"])
                 .interpolate (d3.interpolateHcl);
  
    var xAxis = d3.axisBottom ()
                .scale (Zeitskala)
                .tickValues ​​(tickValues)
                .tickSize (250, 0, 0)
                .tickSizeOuter (0);
    var grid = d3.select ("svg"). append ('g'). call (xAxis);
  
    yScale = d3.scaleLinear (). domain ([0, data.length]). range ([0, 250]);
  
    var projects = d3.select ("svg")
                   .append ('g')
                   .selectAll ("this_is_empty")
                   .data (Daten)
                   .eingeben();
  
    var barWidth = 200;
  
    var innerRects = projects.append ("rect")
                  .attr ("rx", 3)
                  .attr ("ry", 3)
                  .attr ("x", (d, i) => timeScale (dateFormat (d.date)) - barWidth / 2)
                  .attr ("y", (d, i) => yScale (i))
                  .attr ("width", barWidth)
                  .attr ("Höhe", 30)
                  .attr ("Strich", "kein")
                  .attr ("fill", d => d3.rgb (colorScale (categories.indexOf (d.status)));
  
    var rectText = projects.append ("text")
                  .text (d => d.label)
                  .attr ("x", d => timeScale (dateFormat (d.date)))
                  .attr ("y", (d, i) => yScale (i) + 20)
                  .attr ("Schriftgröße", 11)
                  .attr ("text-anchor", "middle")
                  .attr ("Texthöhe", 30)
                  .attr ("fill", "#fff");
}

Und damit haben wir die Rohstruktur unserer Visualisierung.

Gut gemacht.

Erstellen eines wiederverwendbaren Diagramms

Das Ergebnis zeigt, dass es keine Ränder gibt. Wenn wir dieses Diagramm auch auf einer anderen Seite anzeigen möchten, müssen wir den gesamten Code kopieren. Um diese Probleme zu lösen, erstellen wir ein wiederverwendbares Diagramm und importieren es einfach. Um mehr über Diagramme zu erfahren, klicken Sie hier. Um ein früheres Tutorial zu wiederverwendbaren Diagrammen zu sehen, klicken Sie hier.

Die Struktur zum Erstellen eines wiederverwendbaren Diagramms ist immer dieselbe. Ich habe ein Tool erstellt, um eines zu generieren. In diesem Diagramm möchte ich Folgendes festlegen:

  • Die Daten (natürlich)
  • Die Werte für Breite, Höhe und Ränder
  • Eine Zeitskala für den x-Wert der Rechtecke
  • Eine Skala für den y-Wert der Rechtecke
  • Eine Skala für die Farbe
  • Die Werte für xScale, yScale und colorScale
  • Die Werte für den Anfang und das Ende jeder Aufgabe sowie die Höhe der einzelnen Balken

Ich übergebe dies dann an die Funktion, die ich erstellt habe:

Diagramm: ganttAlikeChart
Breite: 800
Höhe: 600
Rand: {oben: 20, rechts: 100, unten: 20, links: 100}
xScale: d3.scaleTime ()
yScale: d3.scaleLinear ()
colorScale: d3.scaleLinear ()
xValue: d => d.date
colorValue: d => d.status
barHeight: 30
Balkenbreite: 100
dateFormat: d3.timeParse ("% d /% m /% Y")

Was gibt mir das:

Funktion ganttAlikeChart () {
Breite = 800;
Höhe = 600;
margin = {top: 20, right: 100, bottom: 20, left: 100};
xScale = d3.scaleTime ();
yScale = d3.scaleLinear ();
colorScale = d3.scaleLinear ();
xValue = d => d.date;
colorValue = d => d.status;
barHeight = 30;
barWidth = 100;
dateFormat = d3.timeParse ("% d /% m /% Y");
Funktionsplan (Auswahl) {
 selection.each (Funktion (Daten) {
  var svg = d3.select (this) .selectAll ("svg"). data ([data]). enter (). append ("svg");
  svg.attr ("width", width + margin.left + margin.right) .attr ("height", height + margin.top + margin.bottom);
  var gEnter = svg.append ("g");
  var mainGroup = svg.select ("g"). attr ("transformieren", "übersetzen (" + margin.left + "," + margin.top + ")");
})}
[...]
Rückgabetabelle;
}

Jetzt müssen wir diese Vorlage nur noch mit dem Code füllen, den wir zuvor erstellt haben. Ich habe auch einige Änderungen am CSS vorgenommen und einen Tooltip hinzugefügt.

Und das ist es.

Sie können den gesamten Code hier auschecken.

Danke fürs Lesen!

Fanden Sie diesen Artikel hilfreich? Ich versuche mein Bestes, jeden Monat einen Deep Dive Artikel zu schreiben. Du kannst eine E-Mail erhalten, wenn ich einen neuen veröffentliche.