Sådan opbygges et Gantt-lignende diagram ved hjælp af D3 til at visualisere et datasæt

Når du er færdig med at lære om det grundlæggende i D3.js, er det næste trin normalt at oprette visualiseringer med dit datasæt. På grund af hvordan D3 fungerer, kan den måde, hvorpå vi organiserer datasættet, gøre vores liv virkelig lette eller virkelig hårde.

I denne artikel vil vi diskutere forskellige aspekter af denne byggeproces. For at illustrere disse aspekter bygger vi en visualisering, der ligner et Gantt-diagram.

Den vigtigste lektion, jeg har lært, er, at du har brug for at oprette et datasæt, hvor hvert datapunkt svarer til en dataenhed i din graf . Lad os dykke ned i vores casestudie for at se, hvordan dette fungerer.

Målet er at opbygge et Gantt-lignende diagram svarende til nedenstående:

Som du kan se, er det ikke et Gantt-diagram, fordi opgaverne starter og slutter samme dag.

Oprettelse af datasættet

Jeg hentede dataene fra minutter. For hver tekstfil modtog jeg oplysninger om projekterne og deres status fra møder. Først strukturerede jeg mine data sådan:

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

Lad os se nærmere på dataene.

Hvert projekt har 4 statusser: presented, voting round 1, voting round 2 og approved. På hvert møde kan status for projekterne ændres eller ikke. Jeg strukturerede dataene ved at gruppere dem efter møder. Denne gruppering gav os en masse problemer, da vi byggede visualiseringen. Dette var fordi vi havde brug for at videregive data til noder med D3. Efter at jeg så Gantt-diagrammet, som Jess Peter byggede her, indså jeg, at jeg var nødt til at ændre mine data.

Hvad var de minimumoplysninger, jeg ville have vist? Hvad var minimumsknudepunktet? Når man ser på billedet, er det informationen om projektet.Så jeg ændrede datastrukturen til følgende:

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

Og alt fungerede bedre efter det. Det er sjovt, hvordan frustrationen forsvandt efter denne enkle ændring.

Oprettelse af visualisering

Nu hvor vi har datasættet, lad os begynde at opbygge visualiseringen.

Oprettelse af x-aksen

Hver dato skal vises på x-aksen. For at gøre det skal du definere d3.timeScale():

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

Minimums- og maksimumværdierne er angivet i arrayet d3.extent().

Nu hvor du har det timeScale, kan du ringe til aksen.

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

Flåtene skal være 250 px lange. Du vil ikke have det ydre kryds. Koden til visning af aksen er:

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

Hvis du plotter dette, kan du se, at der er mange flåter. Faktisk er der flåter for hver dag i måneden. Vi vil kun vise de dage, der havde møder. For at gøre det indstiller vi krydsværdierne eksplicit:

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

Ved hjælp af d3.nest()kan du gruppere alle projekter efter dato (se, hvor praktisk det er at strukturere dataene efter projekter?), Og derefter hente alle datoer og videregive dem til aksen.

Placering af projekterne

Vi er nødt til at placere projekterne langs y-aksen, så lad os definere en ny skala:

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

Domænet er antallet af projekter. Området er størrelsen på hvert kryds. Nu kan vi placere rektanglerne:

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()Og append()altid få en vanskelig opgave. For at bruge enter()metoden (for at oprette en ny node fra et datapunkt) har vi brug for et valg. Derfor har vi brug for selectAll("this_is_empty)", selvom vi endnu ikke har nogen rect. Jeg har brugt dette navn til at præcisere, at vi kun har brug for det tomme valg. Med andre ord bruger vi selectAll("this_is_empty)"til at få et tomt valg, vi kan arbejde på.

Variablen projectshar tomme markeringer afgrænset til data, så vi kan bruge den til at trække projekterne ind innerRects.

Nu kan du også tilføje en etiket til hvert projekt:

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

Farvelægning af hvert projekt

Vi ønsker, at farven på hvert rektangel skal afspejle status for hvert projekt. For at gøre det, lad os oprette en anden 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);

Og så kan vi udfylde rektanglerne med farver fra denne skala. At sammensætte alt, hvad vi har set hidtil, her er koden:

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

Og med det har vi den rå struktur i vores visualisering.

Godt klaret.

Oprettelse af et genanvendeligt diagram

Resultatet viser, at der ikke er nogen margener. Også, hvis vi vil vise denne graf på en anden side, skal vi kopiere hele koden. For at løse disse problemer, lad os oprette et genanvendeligt diagram og bare importere det. Klik her for at lære mere om diagrammer. For at se en tidligere tutorial, skrev jeg om genbrugelige diagrammer, klik her.

Strukturen til at oprette et genanvendeligt diagram er altid den samme. Jeg oprettede et værktøj til at generere et. I denne graf vil jeg indstille:

  • Dataene (selvfølgelig)
  • Værdierne for bredde, højde og margener
  • En tidsskala for xværdien af ​​rektanglerne
  • En skala for y-værdien for rektanglerne
  • En skala for farven
  • Værdierne for xScale, yScaleogcolorScale
  • Værdierne for starten og slutningen af ​​hver opgave og højden på hver bjælke

Jeg sender derefter dette til den funktion, jeg har oprettet:

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

Hvilket giver mig dette:

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

Nu skal vi bare udfylde denne skabelon med den kode, vi oprettede før. Jeg lavede også nogle ændringer i CSS og tilføjede et værktøjstip.

Og det er det.

Du kan tjekke hele koden her.

Tak for læsningen! ?

Fandt du denne artikel nyttig? Jeg prøver mit bedste for at skrive en dybdykartikel hver måned, du kan modtage en e-mail, når jeg offentliggør en ny.