Πώς να δημιουργήσετε ένα γράφημα τύπου Gantt χρησιμοποιώντας το D3 για να απεικονίσετε ένα σύνολο δεδομένων

Όταν τελειώσετε να μαθαίνετε για τα βασικά του D3.js, συνήθως το επόμενο βήμα είναι να δημιουργήσετε οπτικοποιήσεις με το σύνολο δεδομένων σας. Λόγω του τρόπου λειτουργίας του D3, ο τρόπος οργάνωσης του συνόλου δεδομένων μπορεί να κάνει τη ζωή μας πολύ εύκολη ή πολύ δύσκολη.

Σε αυτό το άρθρο θα συζητήσουμε διαφορετικές πτυχές αυτής της διαδικασίας οικοδόμησης. Για να απεικονίσουμε αυτές τις πτυχές, θα δημιουργήσουμε μια οπτικοποίηση παρόμοια με ένα γράφημα Gantt.

Το πιο σημαντικό μάθημα που έμαθα είναι ότι πρέπει να δημιουργήσετε ένα σύνολο δεδομένων όπου κάθε σημείο δεδομένων ισούται με μια ενότητα δεδομένων του γραφήματός σας . Ας δούμε τη μελέτη περίπτωσής μας για να δούμε πώς λειτουργεί.

Ο στόχος είναι να δημιουργήσετε ένα γράφημα τύπου Gantt παρόμοιο με αυτό που ακολουθεί:

Όπως μπορείτε να δείτε, δεν είναι ένα γράφημα Gantt, επειδή οι εργασίες ξεκινούν και τελειώνουν την ίδια μέρα.

Δημιουργία του συνόλου δεδομένων

Έχω εξαγάγει τα δεδομένα από λίγα λεπτά. Για κάθε αρχείο κειμένου, έλαβα πληροφορίες σχετικά με τα έργα και την κατάστασή τους από συναντήσεις. Αρχικά, δομήσαμε τα δεδομένα μου ως εξής:

{ "meetings": [{ "label": "1st Meeting", "date": "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"] } ]}

Ας ρίξουμε μια πιο προσεκτική ματιά στα δεδομένα.

Κάθε έργο έχει 4 καταστάσεις: presented, voting round 1, voting round 2 και approved. Σε κάθε συνάντηση, η κατάσταση των έργων μπορεί ή δεν μπορεί να αλλάξει. Δόμησα τα δεδομένα ομαδοποιώντας τα κατά συσκέψεις. Αυτή η ομαδοποίηση μας έδωσε πολλά προβλήματα όταν δημιουργήσαμε την οπτικοποίηση. Αυτό οφείλεται στο γεγονός ότι χρειαζόμαστε να μεταφέρουμε δεδομένα σε κόμβους με D3. Αφού είδα το γράφημα Gantt που ο Jess Peter δημιούργησε εδώ, συνειδητοποίησα ότι έπρεπε να αλλάξω τα δεδομένα μου.

Ποιες ήταν οι ελάχιστες πληροφορίες που ήθελα να εμφανίσω; Ποιος ήταν ο ελάχιστος κόμβος; Κοιτάζοντας την εικόνα, είναι οι πληροφορίες του έργου.Έτσι άλλαξα τη δομή των δεδομένων ως εξής:

{ "projects": [ { "meeting": "1st Meeting", "type": "project", "date": "09/03/2017", "label": "Project 002/2017", "status": "approved" }, { "meeting": "1st Meeting", "type": "project", "date": "09/03/2017", "label": "Project 005/2017", "status": "voting_round_1" }, { "meeting": "1st Meeting", "type": "project", "date": "09/03/2017", "label": "Project 003/2017", "status": "voting_round_2" }, { "meeting": "1st Meeting", "type": "project", "date": "09/03/2017", "label": "Project 004/2017", "status": "voting_round_2" } ]}

Και όλα λειτουργούσαν καλύτερα μετά από αυτό. Είναι αστείο πώς η απογοήτευση εξαφανίστηκε μετά από αυτήν την απλή αλλαγή.

Δημιουργία της οπτικοποίησης

Τώρα που έχουμε το σύνολο δεδομένων, ας αρχίσουμε να χτίζουμε την οπτικοποίηση.

Δημιουργία του άξονα x

Κάθε ημερομηνία πρέπει να εμφανίζεται στον άξονα x. Για να το κάνετε αυτό, ορίστε d3.timeScale():

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

Οι ελάχιστες και μέγιστες τιμές δίνονται στον πίνακα d3.extent().

Τώρα που έχετε timeScale, μπορείτε να καλέσετε τον άξονα.

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

Τα τσιμπούρια πρέπει να έχουν μήκος 250 εικονοστοιχεία. Δεν θέλετε το εξωτερικό τσιμπούρι. Ο κωδικός για την εμφάνιση του άξονα είναι:

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

Εάν σχεδιάσετε αυτό, μπορείτε να δείτε ότι υπάρχουν πολλά τσιμπούρια. Στην πραγματικότητα, υπάρχουν τσιμπούρια για κάθε ημέρα του μήνα. Θέλουμε να προβάλλουμε μόνο τις ημέρες που είχαν συναντήσεις. Για να το κάνουμε αυτό, θα ορίσουμε ρητά τις τιμές κροτώνων:

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

Χρησιμοποιώντας d3.nest()μπορείτε να ομαδοποιήσετε όλα τα έργα κατά ημερομηνία (δείτε πόσο βολικό είναι να δομήσετε τα δεδομένα ανά έργα;) και, στη συνέχεια, λάβετε όλες τις ημερομηνίες και περάστε τα στον άξονα.

Τοποθέτηση των έργων

Πρέπει να τοποθετήσουμε τα έργα στον άξονα y, οπότε ας καθορίσουμε μια νέα κλίμακα:

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

Ο τομέας είναι ο αριθμός των έργων. Το εύρος είναι το μέγεθος κάθε κρότου. Τώρα μπορούμε να τοποθετήσουμε τα ορθογώνια:

var projects = d3.select("svg") .append('g') .selectAll("this_is_empty") .data(data) .enter();
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("height", 30) .attr("stroke", "none") .attr("fill", "lightblue");

selectAll(), data(), enter()Και append()πάντα δύσκολη. Για να χρησιμοποιήσουμε τη enter()μέθοδο (για να δημιουργήσουμε έναν νέο κόμβο από ένα σημείο δεδομένων), χρειαζόμαστε μια επιλογή. Γι 'αυτό χρειαζόμαστε selectAll("this_is_empty)", ακόμα κι αν δεν έχουμε rectακόμη. Έχω χρησιμοποιήσει αυτό το όνομα για να διευκρινίσω ότι χρειαζόμαστε μόνο την κενή επιλογή. Με άλλα λόγια, χρησιμοποιούμε selectAll("this_is_empty)"για να πάρουμε μια κενή επιλογή στην οποία μπορούμε να εργαστούμε.

Η μεταβλητή projectsέχει άδειες επιλογές που ορίζονται στα δεδομένα, έτσι μπορούμε να την χρησιμοποιήσουμε για να σχεδιάσουμε τα έργα innerRects.

Τώρα μπορείτε επίσης να προσθέσετε μια ετικέτα για κάθε έργο:

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("font-size", 11) .attr("text-anchor", "middle") .attr("text-height", 30) .attr("fill", "#fff");

Χρωματισμός κάθε έργου

Θέλουμε το χρώμα κάθε ορθογωνίου να αντικατοπτρίζει την κατάσταση κάθε έργου. Για να το κάνουμε αυτό, ας δημιουργήσουμε μια άλλη κλίμακα:

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

Και τότε μπορούμε να γεμίσουμε τα ορθογώνια με χρώματα αυτής της κλίμακας. Συγκεντρώνοντας όλα όσα έχουμε δει μέχρι τώρα, εδώ είναι ο κωδικός:

d3.json("projects.json", function(error, data) { chart(data.projetos); });
function chart(data) { var dateFormat = d3.timeParse("%d/%m/%Y"); var timeScale = d3.scaleTime() .domain(d3.extent(data, d => dateFormat(d.date))) .range([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(timeScale) .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(data) .enter(); 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("height", 30) .attr("stroke", "none") .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("font-size", 11) .attr("text-anchor", "middle") .attr("text-height", 30) .attr("fill", "#fff"); }

Και με αυτό έχουμε την πρώτη δομή της οπτικοποίησης μας.

Μπράβο.

Δημιουργία επαναχρησιμοποιήσιμου γραφήματος

Το αποτέλεσμα δείχνει ότι δεν υπάρχουν περιθώρια. Επίσης, εάν θέλουμε να εμφανίσουμε αυτό το γράφημα σε άλλη σελίδα, πρέπει να αντιγράψουμε ολόκληρο τον κώδικα. Για να λύσουμε αυτά τα ζητήματα, ας δημιουργήσουμε ένα επαναχρησιμοποιήσιμο γράφημα και απλώς εισαγάγετέ το. Για να μάθετε περισσότερα σχετικά με τα γραφήματα, κάντε κλικ εδώ. Για να δείτε ένα προηγούμενο σεμινάριο που έγραψα για επαναχρησιμοποιήσιμα γραφήματα, κάντε κλικ εδώ.

Η δομή για τη δημιουργία ενός επαναχρησιμοποιήσιμου γραφήματος είναι πάντα η ίδια. Δημιούργησα ένα εργαλείο για να δημιουργήσω ένα. Σε αυτό το γράφημα, θέλω να ορίσω:

  • Τα δεδομένα (φυσικά)
  • Οι τιμές για πλάτος, ύψος και περιθώρια
  • Μια χρονική κλίμακα για το xαξία των ορθογωνίων
  • Μια κλίμακα για την τιμή y για τα ορθογώνια
  • Μια κλίμακα για το χρώμα
  • Οι τιμές για xScale, yScaleκαιcolorScale
  • Οι τιμές για την έναρξη και το τέλος κάθε εργασίας και το ύψος κάθε ράβδου

Στη συνέχεια το μεταφέρω στη λειτουργία που έχω δημιουργήσει:

chart: ganttAlikeChartwidth: 800height: 600margin: {top: 20, right: 100, bottom: 20, left:100}xScale: d3.scaleTime()yScale: d3.scaleLinear()colorScale: d3.scaleLinear()xValue: d => d.datecolorValue: d => d.statusbarHeight: 30barWidth: 100dateFormat: d3.timeParse("%d/%m/%Y")

Αυτό μου δίνει:

function ganttAlikeChart(){width = 800;height = 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");function chart(selection) { selection.each(function(data) { 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("transform", "translate(" + margin.left + "," + margin.top + ")");})}
[...]
return chart;}

Τώρα πρέπει απλώς να συμπληρώσουμε αυτό το πρότυπο με τον κώδικα που δημιουργήσαμε προηγουμένως. Έκανα επίσης κάποιες αλλαγές στο CSS και πρόσθεσα μια συμβουλή εργαλείου.

Και αυτό είναι.

Μπορείτε να δείτε ολόκληρο τον κωδικό εδώ.

Ευχαριστώ για την ανάγνωση! ;

Βρήκατε αυτό το άρθρο χρήσιμο; Προσπαθώ ό, τι καλύτερο μπορώ να γράφω ένα άρθρο σε βάθος κάθε μήνα, μπορείτε να λαμβάνετε ένα μήνυμα ηλεκτρονικού ταχυδρομείου όταν δημοσιεύω ένα νέο.