
Cuando termine de aprender los conceptos básicos de D3.js, normalmente el siguiente paso es crear visualizaciones con su conjunto de datos. Debido a cómo funciona D3, la forma en que organizamos el conjunto de datos puede hacer que nuestras vidas sean realmente fáciles o realmente difíciles.
En este artículo discutiremos diferentes aspectos de este proceso de construcción. Para ilustrar estos aspectos, crearemos una visualización similar a un diagrama de Gantt.
La lección más importante que aprendí es que necesitas crear un conjunto de datos en el que cada punto de datos sea igual a una unidad de datos de tu gráfico . Analicemos nuestro caso de estudio para ver cómo funciona.
El objetivo es crear un diagrama similar a Gantt similar al siguiente:

Como puede ver, no es un diagrama de Gantt porque las tareas comienzan y terminan el mismo día.
Creando el conjunto de datos
Extraje los datos de los minutos. Para cada archivo de texto, recibí información sobre los proyectos y sus estados de reuniones. Al principio, estructuré mis datos así:
{ "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"] } ]}
Echemos un vistazo más de cerca a los datos.
Cada proyecto tiene 4 estados: presented
, voting round 1
, voting round 2
y approved
. En cada reunión, el estado de los proyectos puede cambiar o no. Estructuré los datos agrupándolos por reuniones. Esta agrupación nos dio muchos problemas cuando construimos la visualización. Esto se debió a que necesitábamos pasar datos a los nodos con D3. Después de ver el diagrama de Gantt que Jess Peter construyó aquí, me di cuenta de que necesitaba cambiar mis datos.
¿Cuál era la información mínima que quería mostrar? ¿Cuál fue el nodo mínimo? Mirando la imagen, es la información del proyecto.Entonces cambié la estructura de los datos a lo siguiente:
{ "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" } ]}
Y todo funcionó mejor después de eso. Es curioso cómo la frustración desapareció después de este simple cambio.
Creando la visualización
Ahora que tenemos el conjunto de datos, comencemos a construir la visualización.
Creando el eje x
Cada fecha debe mostrarse en el eje x. Para hacer eso, defina d3.timeScale()
:
var timeScale = d3.scaleTime() .domain(d3.extent(dataset, d => dateFormat(d.date))) .range([0, 500]);
Los valores mínimo y máximo se dan en la matriz d3.extent()
.
Ahora que lo ha hecho timeScale
, puede llamar al eje.
var xAxis = d3.axisBottom() .scale(timeScale) .ticks(d3.timeMonth) .tickSize(250, 0, 0) .tickSizeOuter(0);
Las garrapatas deben tener 250 píxeles de largo. No quieres la garrapata exterior. El código para mostrar el eje es:
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);}
Si traza esto, puede ver que hay muchas garrapatas. De hecho, hay garrapatas para cada día del mes. Queremos mostrar solo los días que tuvieron reuniones. Para hacer eso, estableceremos los valores de tick explícitamente:
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);
Utilizando d3.nest()
puedes agrupar todos los proyectos por fecha (¿ves lo útil que es estructurar los datos por proyectos?), Y luego obtener todas las fechas y pasarlas al eje.
Colocando los proyectos
Necesitamos colocar los proyectos a lo largo del eje y, así que definamos una nueva escala:
yScale = d3.scaleLinear().domain([0, data.length]).range([0, 250]);
El dominio es el número de proyectos. El rango es el tamaño de cada tick. Ahora podemos colocar los rectángulos:
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()
Y append()
siempre conseguir difícil. Para usar el enter()
método (para crear un nuevo nodo a partir de un punto de datos), necesitamos una selección. Por eso lo necesitamos selectAll("this_is_empty)"
, incluso si aún no tenemos ninguno rect
. He usado este nombre para aclarar que solo necesitamos la selección vacía. En otras palabras, usamos selectAll("this_is_empty)"
para obtener una selección vacía en la que podemos trabajar.
La variable projects
tiene selecciones vacías limitadas a datos, por lo que podemos usarla para dibujar los proyectos innerRects
.
Ahora también puede agregar una etiqueta para cada proyecto:
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");
Colorear cada proyecto
Queremos que el color de cada rectángulo refleje el estado de cada proyecto. Para hacer eso, creemos otra escala:
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);
Y luego podemos llenar los rectángulos con colores de esta escala. Juntando todo lo que hemos visto hasta ahora, aquí está el código:
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"); }
Y con eso tenemos la estructura en bruto de nuestra visualización.
Bien hecho.
Crear un gráfico reutilizable
El resultado muestra que no hay márgenes. Además, si queremos mostrar este gráfico en otra página, debemos copiar el código completo. Para resolver estos problemas, creemos un gráfico reutilizable e importémoslo. Para obtener más información sobre gráficos, haga clic aquí. Para ver un tutorial anterior que escribí sobre gráficos reutilizables, haga clic aquí.
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
, andcolorScale
- 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.