Hogyan készítsünk Gantt-szerű diagramot az adatkészlet vizualizálásához a D3 segítségével

Amikor befejezi a D3.js alapjainak megismerését, általában a következő lépés az vizualizációk készítése az adatkészlettel. A D3 működése miatt az adatkészlet rendezése valóban megkönnyítheti vagy megnehezítheti az életünket.

Ebben a cikkben ennek az építési folyamatnak a különböző aspektusait fogjuk megvitatni. Ezen szempontok szemléltetésére egy Gantt-diagramhoz hasonló megjelenítést készítünk.

A legfontosabb lecke, amit megtanultam, hogy fel kell építenie egy adatkészletet, ahol minden egyes adatpont megegyezik a grafikon adategységével . Merüljünk bele esettanulmányunkba, hogy lássuk, hogyan működik ez.

A cél az alábbihoz hasonló Gantt-szerű diagram elkészítése:

Mint látható, ez nem egy Gantt-diagram, mert a feladatok ugyanazon a napon kezdődnek és fejeződnek be.

Az adatkészlet létrehozása

Percekből kivontam az adatokat. Minden szöveges fájl esetében az ülésekről információkat kaptam a projektekről és azok állapotáról. Eleinte az alábbiak szerint strukturáltam az adataimat:

{ "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"] } ]}

Vizsgáljuk meg közelebbről az adatokat.

Minden projekt 4 állapotok: presented, voting round 1, voting round 2 és approved. Minden találkozón a projektek állapota megváltozhat vagy nem. Az adatokat úgy szerveztem, hogy értekezletek alapján csoportosítottam őket. Ez a csoportosítás sok problémát okozott nekünk, amikor felépítettük a vizualizációt. Ennek oka az volt, hogy adatokat kellett átadnunk a D3-as csomópontoknak. Miután megláttam a Gantt-diagramot, amelyet Jess Peter készített itt, rájöttem, hogy meg kell változtatnom az adataimat.

Mi volt a minimális információ, amelyet meg akartam jeleníteni? Mi volt a minimális csomópont? A képet nézve ez a projekt információja.Tehát az adatok szerkezetét a következőkre változtattam:

{ "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" } ]}

És utána minden jobban működött. Vicces, hogyan tűnt el a frusztráció ezen egyszerű változás után.

A megjelenítés készítése

Most, hogy megvan az adatkészlet, kezdjük el a vizualizáció felépítését.

Az x tengely létrehozása

Minden dátumot az x tengelyen kell megjeleníteni. Ehhez adja meg d3.timeScale():

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

A minimális és a maximális érték a tömbben van megadva d3.extent().

Most, hogy megvan timeScale, felhívhatja a tengelyt.

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

A kullancsoknak 250 képpont hosszúaknak kell lenniük. Nem akarod a külső pipát. A tengely megjelenítésére szolgáló kód:

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

Ha ezt megtervezi, láthatja, hogy sok a kullancs. Valójában vannak kullancsok a hónap minden napjára. Csak azokat a napokat szeretnénk megjeleníteni, amelyeken találkozók voltak. Ehhez kifejezetten beállítjuk a pipa értékeket:

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

A használatával az d3.nest()összes projektet dátum szerint csoportosíthatja (megnézheti, mennyire hasznos az adatok projektek szerinti strukturálása?), Majd megkapja az összes dátumot és továbbítja a tengelyhez.

A projektek elhelyezése

A projekteket az y tengely mentén kell elhelyeznünk, ezért határozzunk meg egy új skálát:

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

A domain a projektek száma. A tartomány az egyes kullancsok mérete. Most elhelyezhetjük a téglalapokat:

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()És append()mindig trükkös. A enter()módszer használatához (új csomópont létrehozásához egy adatpontból) szükségünk van egy kiválasztásra. Ezért van szükségünk selectAll("this_is_empty)", még akkor is, ha még nincs rect. Ezzel a névvel tisztáztam, hogy csak az üres választásra van szükségünk. Más szavakkal, selectAll("this_is_empty)"egy üres választást kapunk, amelyen dolgozhatunk.

A változó projectsüres szelekcióval rendelkezik, amelyek adatokhoz vannak kötve, így felhasználhatjuk a projektek bevonására innerRects.

Most hozzáadhat címkét is minden projekthez:

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

Minden projekt színezése

Azt akarjuk, hogy az egyes téglalapok színe tükrözze az egyes projektek állapotát. Ehhez hozzunk létre egy másik skálát:

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

És akkor a téglalapokat kitölthetjük a skála színével. Összeszedve mindent, amit eddig láttunk, itt van a kód:

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

És ezzel megvan a vizualizációnk nyers szerkezete.

Szép munka.

Újrafelhasználható diagram létrehozása

Az eredmény azt mutatja, hogy nincsenek margók. Továbbá, ha ezt a grafikont egy másik oldalon szeretnénk megjeleníteni, akkor a teljes kódot át kell másolnunk. E problémák megoldásához készítsünk újrafelhasználható diagramot, és csak importáljuk. Ha többet szeretne megtudni a diagramokról, kattintson ide. Egy korábbi oktatóanyag megtekintéséhez, amelyet újrafelhasználható táblázatokról írtam, kattintson ide.

The structure to create a reusable chart is always the same. I created a tool to generate one. In this graph, I want to set:

  • The data (of course)
  • The values for width, height, and margins
  • A time scale for the xvalue of the rectangles
  • A scale for the y value for the rectangles
  • A scale for the color
  • The values for xScale, yScale , and colorScale
  • The values for the start and end of each task and the height of each bar

I then pass this to the function I've created:

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

Which gives me this:

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

Now we just need to fill this template with the code we created before. I also made some changes to the CSS and added a tooltip.

And that's it.

You can check out the entire code here.

Thanks for reading! ?

Did you found this article helpful? I try my best to write a deep dive article each month, you can receive an email when I publish a new one.