Graphe à barres empilées D3, avec chaque pile d'une couleur différente définie par les différents groupes,

Nov 24 2020

Le problème

J'essaie d'obtenir un graphique à barres empilées dans D3 (v5) pour avoir une barre colorée individuellement pour différents groupes (ce que je peux faire, figure 1), avec chaque pile d'une couleur différente (en fonction de la figure 2).

Je ne peux pas trouver un moyen d'obtenir la coloration de la pile (c'est-à-dire que je veux que les différentes nuances de la couleur du groupe varient avec la hauteur de la pile différente) dans l'exemple de la figure 3 (sauf que j'aimerais que les différents groupes soient de couleurs différentes, c'est-à-dire ne pas se répéter comme ils sont ici).

Dans les exemples de code que j'ai fournis, il existe deux ensembles de données. Un ensemble simple, pour aider à jouer avec les données:

Animal,Group,A,B,C,D
Dog,Domestic,10,10,20,5
Cat,Domestic,20,5,10,10
Mouse,Pest,75,5,35,0 
Lion,Africa,5,5,30,25
Elephant,Africa,15,15,20,20
Whale,Marine,35,20,10,45
Shark,Marine,45,55,0, 60
Fish,Marine,20, 5,30,10

Et un ensemble plus grand que j'essaie réellement d'utiliser. Voici le bl.ocks.orgcode que j'essaye de développer:

// set the dimensions and margins of the graph
const margin = {
    top: 90,
    right: 20,
    bottom: 30,
    left: 40
  },
  width = 960 - margin.left - margin.right,
  height = 960 - margin.top - margin.bottom;

const svg = d3.select("#my_dataviz")
  .append("svg")
  .attr("width", width + margin.left + margin.right)
  .attr("height", height + margin.top + margin.bottom)
  .append("g")
  .attr("transform",
    "translate(" + margin.left + "," + margin.top + ")");

const y = d3.scaleBand()
  .range([0, height])
  .padding(0.1);
const x = d3.scaleLinear()
  .range([0, width]);
const z = d3.scaleOrdinal()
  .range(["none", "lightsteelblue", "steelblue", "darksteelblue"]);

d3.csv("https://gist.githubusercontent.com/JimMaltby/844ca313589e488b249b86ead0d621a9/raw/f328ad6291ffd3c9767a2dbdba5ce8ade59a5dfa/TimeBarDummyFormat.csv", d3.autoType, (d, i, columns) => {
      var i = 3;
      var t = 0;
      for (; i < columns.length; ++i)
        t += d[columns[i]] = +d[columns[i]];
      d.total = t;
      return d;
    }

  ).then(function(data) {
    const keys = data.columns.slice(3); // takes the column names, ignoring the first 3 items = ["EarlyMin","EarlyAll", "LateAll", "LateMax"]

    // List of groups = species here = value of the first column called group -> I show them on the X axis
    const Groups = d3.map(data, d => d.Group);
    y.domain(data.map(d => d.Ser));
    x.domain([2000, d3.max(data, d => d.total)]).nice();
    z.domain(keys);

    svg.append("g")
      .selectAll("g")
      .data(d3.stack().keys(keys)(data))
      .enter().append("g")
      .attr("fill", d => z(d.key)) //Color is assigned here because you want everyone for the series to be the same color
      .selectAll("rect")
      .data(d => d)
      .enter()
      .append("rect")
      .attr("y", d => y(d.data.Ser))
      .attr("x", d => x(d[0]))
      .attr("height", y.bandwidth())
      .attr("width", d => x(d[1]) - x(d[0]));

    svg.append("g")
      .attr("transform", "translate(0,0)")
      .call(d3.axisTop(x));

    svg.append("g")
      .call(d3.axisLeft(y));
  });
.bar {
  fill: rgb(70, 131, 180);
}
<script src="https://d3js.org/d3.v5.min.js"></script>
<div id="my_dataviz"></div>

JSFiddle: https://jsfiddle.net/yq7bkvdL/

Ce que j'ai essayé

J'ai l'impression qu'il me manque quelque chose de simple, mais je suis un noob de codage et mon codage est assez rudimentaire, donc je ne peux pas le résoudre.

Je pense que je place le remplissage attrau mauvais endroit. Ou c'est que je ne comprends pas comment sélectionner la clé dans les données imbriquées / hiérarchiques de d3.stack.

J'ai essayé diverses choses, toutes sans succès:

1. Tableau de couleurs J'ai essayé d'écrire une fonction pour créer un tableau de couleurs, en itérant (avec forEach) sur les valeurs / noms de "clé" et de "Groupe" et de les concaténer pour créer un tableau que je peux utiliser avec le d3 Échelle (ordinale) pour sélectionner la bonne couleur. Par exemple, avec le premier jeu de données, il créerait un tableau ColoursID [DomesticA, DomesticB, DomesticC, DomesticD,PestA, PestB...]qui correspondrait ensuite aux couleurs deColourS ["grey", "lightgreen", "green", "darkgreen", "yellow", ...]

Vous trouverez ci-dessous la tentative de le faire, ainsi que diverses autres explorations commentées.

  // List of groups = species here = value of the first column called group -> I show them on the X axis
  const stack = d3.stack().keys(stackKeys)(data);

//const Groups = d3.map(data, d => d.Group);
//const ColourID = d3.map(data, d => d.Group && d.key);
//  const stackID = stack.data // //stack[3].key = "D" // [2][6].data.Group  = "Marine"
// const Test1 = Object.entries(stack).forEach(d => d.key);
        
const stackB = stack.forEach(function(d,i,j){
                                                //var a =  Object.keys(d)//= list of 3rd Level keys "0,..7,key,index"
                                                //var a =  Object.keys(d).forEach((key) => key) "undefined"
                                                //var   a = d.key //= "D" for all
                        d.forEach(function(d,i){
                                            //var a =  d.keys // = "function keys{} ([native code])"
                          //var a =  Object.keys(d)
                          //var a =  Object.keys(d) //= list of 2nd Level keys "0,1,data"
                                                    var  b = data[i]["Group"]   
                                    d.forEach(function(d){
                                                    //var a = [d]["key"] // = "undefined"
                          //var a = Object.keys(d).forEach((key) => d[key]) // = "undefined"
                              var a = Object.keys(d) //= ""
                           //   var a =  d.keys //= "undefined"
                                data[i]["colourID"] = b + " a" + "-b " + a //d.key
                                                                })  
                        })    
               });
  
  console.log(stack)
      svg.append("g")
        .selectAll("g")
        .data(stack)
        .enter().append("g")   
        //.attr("fill", d => z(d.data.Group) ) //Color is assigned here because you want everyone for the series to be the same color
        .selectAll("rect")
        .data(d => d)
        .enter().append("rect")
        .attr("fill", d => colourZ(d.data.colourID)) //Color is assigned here because you want each Group to be a different colour **how do you vary the blocks?**
        .attr("y", d => y(d.data.Animal) )      //uses the Column of data Animal to seperate bars
        .attr("x", d=> x(d[0]) )                //
        .attr("height", y.bandwidth() )         //
        .attr("width", d=> x(d[1]) - x(d[0]));  //
            

Code VizHub: https://vizhub.com/JimMaltby/373f1dbb42ad453787dc0055dee7db81?file=index.js

2. Créez une deuxième échelle de couleurs:

J'ai utilisé le conseil ici ( d3.js - ajouter différentes couleurs à une barre dans un graphique à barres empilées ), en ajoutant une fonction if pour sélectionner une échelle de couleurs différente, en ajoutant ce code:

//-----------------------------BARS------------------------------//

      // append the rectangles for the bar chart
      svg.append("g")
        .selectAll("g")
        .data(stack)
        .enter().append("g")      

            //Color is assigned here because you want everyone for the series to be the same color
        .selectAll("rect")
        .data(d => d)
        .enter().append("rect")
        .attr("fill", d => 
              d.data.Group == "Infrastructure" 
              ? z2(d.key) 
              : z(d.key))
        //.attr("class", "bar")
        .attr("y", d => y(d.data.Ser) )         //Vert2Hor **x to y** **x(d.data.Ser) to y(d.data.Ser)**
        .attr("x", d=> x(d[0]) )                //Vert2Hor **y to x** **y(d[1]) to x(d[0])**
        .attr("height", y.bandwidth() )         //Vert2Hor **"width" to "height"**  **x.bandwidth to y.bandwidth** 
        .attr("width", d=> x(d[1]) - x(d[0]));  //Vert2Hor **"height" to "width"**  **y(d[0]) - y(d[1]) to x(d[1]) - x(d[0])**

Code VizHub

3. Une grande fonction IF en remplissage.

Si c'est la solution, j'apprécierais quelques conseils sur un. faire fonctionner, puis b. avoir une manière plus efficace de le faire

Là encore, il semble que j'ai du mal à sélectionner la «clé» du tableau de données «pile». Vous noterez que j'ai essayé différentes manières de sélectionner la clé dans le code ici, sans succès :(.

        .attr("fill", function(d,i, j) {        
            if (d.data.Group === "Domestic") {
                if (d.key === "A") { return "none"}
                    else if (d.key === "B") { return "lightblue"}
                    else if (d.key === "C") { return "blue"}
                else if (d.key === "D") { return "darkblue"}
                else  { return "forestgreen"}
            }
            else if (d.data.Group === "Pest") {
                if (d.key === "A") { return "yellow"}
                    else if (d.key === "B") { return "lightorange"}
                    else if (d.key === "C") { return "orange"}
                else if (d.key === "D") { return "darkorange"}
                else  { return "Brown"} //"yellow", "lightorange", "orange", ""
            }
            else if (d.data.Group === "Africa") {
                if (Object.keys(root.data) === 1) { return "grey"}
                    else if (d.key === "B") { return "lightred"}
                    else if (d.key === "C") { return "red"}
                else if (d.key === "D") { return "darkred"}
                else  { return "pink"}
            }
            else if (d.data.Group == "Marine") {
                if (stackKeys == "A") { return "lightgrey"}
                    else if (stackKeys[d] == "B") { return "lightblue"}
                    else if (stackKeys[i] == "C") { return "blue"}
                else if (stackKeys[3] == "D") { return "darkblue"}
                else  { return "steelblue"}
            }
            else { return "black" }             
                ;})

Code dans Viz Hub

Réponses

RubenHelsloot Nov 25 2020 at 05:51

Si vous souhaitez varier légèrement les couleurs des barres si les barres sont de plus petite longueur, vous pouvez utiliser fill-opacityet conserver fillles mêmes! De cette façon, les couleurs sont moins prononcées et plus claires si la valeur est plus claire.

Créez simplement une nouvelle balance opacityavec gamme [0.3, 1]. J'ai choisi 0,3 parce que 0 opacité signifie que la barre est invisible, et vous ne le voulez généralement pas. J'ai ajouté une valeur distincte d.heightpour désigner toute la hauteur visible de la barre, qui est distincte du début (mais équivalente à d.Min + d.All + d.Max). Maintenant, appliquez simplement l'attribut à chaque barre et vous avez terminé.

Vous pouvez choisir de définir la plage sur [0, 1]et de ne pas l'utiliser d3.extentpour le domaine - cela conduira probablement à des résultats similaires, bien qu'il existe certaines différences que vous pouvez repérer avec une expérience de pensée.

À l'heure actuelle, l' fill-opacityattribut est défini sur chaque barre. Ainsi, les barres de la même pile ont la même fill-opacityvaleur. Notez que cela est entièrement facultatif, cependant, et que vous pouvez également appliquer des valeurs distinctes.

// set the dimensions and margins of the graph
const margin = {
    top: 90,
    right: 20,
    bottom: 30,
    left: 40
  },
  width = 960 - margin.left - margin.right,
  height = 960 - margin.top - margin.bottom;

const svg = d3.select("#my_dataviz")
  .append("svg")
  .attr("width", width + margin.left + margin.right)
  .attr("height", height + margin.top + margin.bottom)
  .append("g")
  .attr("transform",
    "translate(" + margin.left + "," + margin.top + ")");

const y = d3.scaleBand()
  .range([0, height])
  .padding(0.1);
const x = d3.scaleLinear()
  .range([0, width]);
const z = d3.scaleOrdinal()
  .range(["none", "lightsteelblue", "steelblue", "darksteelblue"]);
const opacity = d3.scaleLinear()
  .range([0.3, 1]);

d3.csv("https://gist.githubusercontent.com/JimMaltby/844ca313589e488b249b86ead0d621a9/raw/f328ad6291ffd3c9767a2dbdba5ce8ade59a5dfa/TimeBarDummyFormat.csv", d3.autoType, (d, i, columns) => {
      var i = 3;
      var t = 0;
      for (; i < columns.length; ++i)
        t += d[columns[i]] = +d[columns[i]];
      d.total = t;
      d.height = d.total - d.Start;
      return d;
    }

  ).then(function(data) {
    const keys = data.columns.slice(3); // takes the column names, ignoring the first 3 items = ["EarlyMin","EarlyAll", "LateAll", "LateMax"]

    // List of groups = species here = value of the first column called group -> I show them on the X axis
    const Groups = d3.map(data, d => d.Group);
    y.domain(data.map(d => d.Ser));
    x.domain([2000, d3.max(data, d => d.total)]).nice();
    z.domain(keys);
    opacity.domain(d3.extent(data, d => d.height));
    console.log(opacity.domain());

    svg.append("g")
      .selectAll("g")
      .data(d3.stack().keys(keys)(data))
      .enter().append("g")
      .attr("fill", d => z(d.key))
      .selectAll("rect")
      .data(d => d)
      .enter()
      .append("rect")
      .attr("y", d => y(d.data.Ser))
      .attr("x", d => x(d[0]))
      .attr("height", y.bandwidth())
      .attr("width", d => x(d[1]) - x(d[0]))
      .attr("fill-opacity", d => opacity(d.data.height));

    svg.append("g")
      .attr("transform", "translate(0,0)")
      .call(d3.axisTop(x));

    svg.append("g")
      .call(d3.axisLeft(y));
  });
.bar {
  fill: rgb(70, 131, 180);
}
<script src="https://d3js.org/d3.v5.min.js"></script>
<div id="my_dataviz"></div>


Edit : sachant que vous souhaitez colorer les barres par groupe, j'utiliserais la même logique, mais faire quelques ajustements:

D'une part, j'ai changé zpour traiter fill-opacity(que j'utilise toujours pour accentuer les différents groupes), et utiliser une nouvelle échelle ordinale grouppour les couleurs. La clé de cette échelle est simplement le champ préexistant d.Group.

// set the dimensions and margins of the graph
const margin = {
    top: 90,
    right: 20,
    bottom: 30,
    left: 40
  },
  width = 960 - margin.left - margin.right,
  height = 960 - margin.top - margin.bottom;

const svg = d3.select("#my_dataviz")
  .append("svg")
  .attr("width", width + margin.left + margin.right)
  .attr("height", height + margin.top + margin.bottom)
  .append("g")
  .attr("transform",
    "translate(" + margin.left + "," + margin.top + ")");

const y = d3.scaleBand()
  .range([0, height])
  .padding(0.1);
const x = d3.scaleLinear()
  .range([0, width]);
const z = d3.scaleOrdinal()
  .range([0.25, 0.5, 0.75, 1]);
const group = d3.scaleOrdinal()
  .range(["darkgreen", "darkred", "steelblue", "purple"]);

d3.csv("https://gist.githubusercontent.com/JimMaltby/844ca313589e488b249b86ead0d621a9/raw/f328ad6291ffd3c9767a2dbdba5ce8ade59a5dfa/TimeBarDummyFormat.csv", d3.autoType, (d, i, columns) => {
      var i = 3;
      var t = 0;
      for (; i < columns.length; ++i)
        t += d[columns[i]] = +d[columns[i]];
      d.total = t;
      return d;
    }

  ).then(function(data) {
    const keys = data.columns.slice(3); // takes the column names, ignoring the first 3 items = ["EarlyMin","EarlyAll", "LateAll", "LateMax"]

    y.domain(data.map(d => d.Ser));
    x.domain([2000, d3.max(data, d => d.total)]).nice();
    z.domain(keys);
    group.domain(data.map(d => d.Group));

    svg.append("g")
      .selectAll("g")
      .data(d3.stack().keys(keys)(data))
      .enter().append("g")
      .attr("fill-opacity", d => z(d.key))
      .selectAll("rect")
      .data(d => d)
      .enter()
      .append("rect")
      .attr("y", d => y(d.data.Ser))
      .attr("x", d => x(d[0]))
      .attr("height", y.bandwidth())
      .attr("width", d => x(d[1]) - x(d[0]))
      .attr("fill", d => group(d.data.Group));
      

    svg.append("g")
      .attr("transform", "translate(0,0)")
      .call(d3.axisTop(x));

    svg.append("g")
      .call(d3.axisLeft(y));
  });
.bar {
  fill: rgb(70, 131, 180);
}
<script src="https://d3js.org/d3.v5.min.js"></script>
<div id="my_dataviz"></div>


Edit 2 : si vous souhaitez spécifier les couleurs vous-même, j'utiliserais une carte de touches aux couleurs:

// set the dimensions and margins of the graph
const margin = {
    top: 90,
    right: 20,
    bottom: 30,
    left: 40
  },
  width = 960 - margin.left - margin.right,
  height = 960 - margin.top - margin.bottom;

const svg = d3.select("#my_dataviz")
  .append("svg")
  .attr("width", width + margin.left + margin.right)
  .attr("height", height + margin.top + margin.bottom)
  .append("g")
  .attr("transform",
    "translate(" + margin.left + "," + margin.top + ")");

const y = d3.scaleBand()
  .range([0, height])
  .padding(0.1);
const x = d3.scaleLinear()
  .range([0, width]);
const z = d3.scaleOrdinal()
  .range([0.25, 0.5, 0.75, 1]);
const group = d3.scaleOrdinal()
  .range([
    { Start: "none", Min: "lightgreen", All: "green", Max: "darkgreen" },
    { Start: "none", Min: "indianred", All: "red", Max: "darkred" },
    { Start: "none", Min: "lightsteelblue", All: "steelblue", Max: "darksteelblue" }
  ]);

d3.csv("https://gist.githubusercontent.com/JimMaltby/844ca313589e488b249b86ead0d621a9/raw/f328ad6291ffd3c9767a2dbdba5ce8ade59a5dfa/TimeBarDummyFormat.csv", d3.autoType, (d, i, columns) => {
      var i = 3;
      var t = 0;
      for (; i < columns.length; ++i)
        t += d[columns[i]] = +d[columns[i]];
      d.total = t;
      return d;
    }

  ).then(function(data) {
    const keys = data.columns.slice(3); // takes the column names, ignoring the first 3 items = ["EarlyMin","EarlyAll", "LateAll", "LateMax"]

    y.domain(data.map(d => d.Ser));
    x.domain([2000, d3.max(data, d => d.total)]).nice();
    z.domain(keys);
    group.domain(data.map(d => d.Group));

    svg.append("g")
      .selectAll("g")
      .data(d3.stack().keys(keys)(data))
      .enter().append("g")
      .each(function(e) {
        d3.select(this)
          .selectAll("rect")
          .data(d => d)
          .enter()
          .append("rect")
          .attr("y", d => y(d.data.Ser))
          .attr("x", d => x(d[0]))
          .attr("height", y.bandwidth())
          .attr("width", d => x(d[1]) - x(d[0]))
          .attr("fill", d => group(d.data.Group)[e.key]);
      });
      

    svg.append("g")
      .attr("transform", "translate(0,0)")
      .call(d3.axisTop(x));

    svg.append("g")
      .call(d3.axisLeft(y));
  });
.bar {
  fill: rgb(70, 131, 180);
}
<script src="https://d3js.org/d3.v5.min.js"></script>
<div id="my_dataviz"></div>