Using D3, how to iterate over a non-uniform matrix (of categories) and draw rectangles based on category-color map?

Viewed 36

I am just starting with D3 and React, perhaps that's why I have been unable to solve this simple problem since a few days.

I have a CSV file from which I read columns, and one column is an array that I need to plot. Since I am new to both D3 and React, I thought let's first plot a 1D array and then move onto 2D arrays. I have been successful with the 1D array, but no matter what I try I cannot get the 2D array to work.

For simplicity let's hard-code both the data for now.

The initial 1D array was -

const emotions = ['neutral', 'surprise', 'disgust', 'fear', 'sadness', 'fear', 'joy', 'sadness', 'joy'];

Which would later become 2D as follows -

const emotions = [
['neutral', 'surprise', 'disgust', 'fear', 'sadness', 'fear', 'joy', 'sadness', 'joy'],
['sadness', 'sadness', 'joy', 'joy', 'joy']
];

I made a scale to map the categories with the colors as follows -

const emoScale = d3.scaleOrdinal()
        .range(['red', 'green', 'purple', 'yellow', 'white', 'gray', 'orange'])
        .domain(['anger', 'disgust', 'fear', 'joy', 'neutral', 'sadness', 'surprise']);

After which I tried successfully to show the 1D array like -

    const emotion1D = ['neutral', 'surprise', 'disgust', 'fear', 'sadness', 'fear', 'joy', 'sadness', 'joy'];

    const emoScale = d3.scaleOrdinal()
        .range(['red', 'green', 'purple', 'yellow', 'white', 'gray', 'orange'])
        .domain(['anger', 'disgust', 'fear', 'joy', 'neutral', 'sadness', 'surprise']);
    console.log(emoScale("anger"));
          
    const d3Chart = useRef();
   
    const svg = d3.select(d3Chart.current);
        var selection = svg.selectAll("rect").data(emotion1D);
        // var yScale = emoScale;
        selection
            .enter()
            .append("rect")
            .attr("x", (d, i) => 0)
            .attr("y", (d, i) => i*11)
            .attr("width", 10)
            .attr("height", 10)
            .attr("fill", function(d) { return emoScale(d); })

1D rects

And the output was a stacked bar of rects of different colors - which is exactly what I need. Now I need to show this for as many rows I have in the matrix. So if I get it working on a 2D matrix, then my problem is solved.

So finally, I should be able to see another bar of stacked rects of different colors and a different total size, right next to the first one...and so on depending on how many rows I have and how many elements are inside it.

While searching for how to render 2D array, I came across Nested Selection. So that's how I tried to solve this. The idea being that for the 1D array in the matrix, the Y position remains the same, X changes by x+10. And for data in 1D array, Y changes by y*rect_height and X remains same as above.

I have tried playing around with many changes to svg group, x and y positions, but nothing gave what I want. At some point I got a diagonal, but that's not what I want. The current code that I have is as follows...I am not sure why the display is not consistent...it seems to display few diagonal rects when I change something, but refreshing it always gives me a blank screen.

    const emotion2D = [['neutral', 'surprise', 'disgust', 'fear', 'sadness', 'fear', 'joy', 'sadness', 'joy'],['sadness', 'sadness', 'joy', 'joy', 'joy']];

    const emotion1D = ['neutral', 'surprise', 'disgust', 'fear', 'sadness', 'fear', 'joy', 'sadness', 'joy'];

    const emoScale = d3.scaleOrdinal()
        .range(['red', 'green', 'purple', 'yellow', 'white', 'gray', 'orange'])
        .domain(['anger', 'disgust', 'fear', 'joy', 'neutral', 'sadness', 'surprise']);
    console.log(emoScale("anger"));

    const d3Chart = useRef();

    const svg = d3.select(d3Chart.current);
        var selection = svg.selectAll("rect").data(emotion2D);

        selection
        .enter()
        .append("g")
        .attr("x", (d, i) => i+10)
        .attr("y", (d, i) => 0)
        .selectAll("rect")
        .data(function(d,i){
            return d[i];
          })
        .enter()
        .append("rect")
        .attr("x", (d, i) => 0)
        .attr("y", (d, i) => i*11)
        .attr("width", 10)
        .attr("height", 10)
        .attr("fill", function(d) { return emoScale(d); })

The full react files are as follows -

Chart.jsx

import React, { Component } from 'react';
import * as d3 from 'd3';
import { useRef } from 'react';

const Chart = function() {      
    const emotion2D = [['neutral', 'surprise', 'disgust', 'fear', 'sadness', 'fear', 'joy', 'sadness', 'joy'],['sadness', 'sadness', 'joy', 'joy', 'joy']];

    const emotion1D = ['neutral', 'surprise', 'disgust', 'fear', 'sadness', 'fear', 'joy', 'sadness', 'joy'];

    const emoScale = d3.scaleOrdinal()
        .range(['red', 'green', 'purple', 'yellow', 'white', 'gray', 'orange'])
        .domain(['anger', 'disgust', 'fear', 'joy', 'neutral', 'sadness', 'surprise']);
    console.log(emoScale("anger"));
          
    const d3Chart = useRef();
 
    const svg = d3.select(d3Chart.current);
        var selection = svg.selectAll("rect").data(emotion2D);
        // var yScale = emoScale;

        selection
        .enter()
        .append("g")
        .attr("x", (d, i) => i+10)
        .attr("y", (d, i) => 0)
        .selectAll("rect")
        .data(function(d,i){
            return d[i];
          })
        .enter()
        .append("rect")
        .attr("x", (d, i) => 0)
        .attr("y", (d, i) => i*11)
        .attr("width", 10)
        .attr("height", 10)
        .attr("fill", function(d) { return emoScale(d); })

    return(
        <div>
            <svg style={{border: "1px solid black"}} ref={d3Chart}>
            </svg>
        </div>
    )
}
export default Chart;

App.js

import './App.css';
import Chart from './Chart';

function App() {

  return (
     <Chart />
  );
}

export default App;
1 Answers

I was not grouping correctly. The following code seems to make it work -

var groups = svg.selectAll("g")
                    .data(emotion2D)
                    .join('g')
                    .attr("transform", (d, i) => 'translate(' + (11 * i) + ',0)');
       
    groups
        .selectAll("rect")
        .enter()
        .data(function(d,i){
            return d;
          })
        .join('rect')
        .attr("x", (d, i) => 0)
        .attr("y", (d, i) => 140-(i*11))
        .attr("width", 10)
        .attr("height", 10)
        .attr("fill", function(d) { return emoScale(d); })
Related