How to build a compartmentalized heatmap

Viewed 125

Using D3, I want to take the data visualization type of a classical heatmap...

enter image description here

.. onto a compartmentalized version of several heatmap groups drawing data from a single data source.

enter image description here

Technically this should be one heatmap element drawing its data from a single source - separation and thus clustering/grouping is supposed to happen through sorting the data in the *.csv file (group one, group two, group three..) and the D3 *.JS file handling the styling.

While generating a single map:

    // Build X scales and axis:
    const x = d3.scaleBand()
    .range([0, width])
    .domain(myGroups)
    .padding(0.00);
    svg.append('g')
    .attr('transform', `translate(0,${height})`)
    .call(d3.axisBottom(x));

    // Build Y scales and axis:
    const y = d3.scaleBand()
    .range([height, 0])
    .domain(myVars)
    .padding(0.00);
    svg.append('g')
    .call(d3.axisLeft(y));

assigning a color:

      // Assign color scale
    const myColor = d3.scaleLinear()
    .range(['red', '#750606'])
    .domain([1, 100]);

and fetching (sample) data:

// Read the data
d3.csv('https://raw.githubusercontent.com/holtzy/D3-graph-gallery/master/DATA/heatmap_data.csv', (data) => {
  data.sort(function(a, b) {
    return myVars.indexOf(b.variable) - myVars.indexOf(a.variable) || myGroups.indexOf(a.group) - myGroups.indexOf(b.group)
  });

Has been working like a charm: CodePen

I'm struggling to expand this basic structure onto the generation of multiple groups as described above. Expanding the color scheme, trying to build several additional X and Y axis that cover different ranges result in a complete break of the D3 element rendering the map unable to be displayed at all.

Can someone point me in the right direction on how to generate multiple heatmap groups without breaking the heatmap?

1 Answers

I was able to solve the compartmentalization using a row and column based procedure to construct the compartments:

    // Dimensions
  const numCategoryCols = 4;
  const numCategoryRows = Math.ceil(grouped.length / numCategoryCols);
  const numEntryCols = 3;
  const numEntryRows = Math.ceil(grouped[0].values.length / numEntryCols);
  const gridSize = 20;
  const width = gridSize * numCategoryCols * numEntryCols;
  const height = gridSize * numCategoryRows * numEntryRows;
  const tooltipArrowSize = 8;

  // Containers
  const container = d3
    .select("#" + containerId)
    .classed("heatmap-grid", true)
    .style("position", "relative");
  const svg = container
    .append("svg")
    .style("display", "block")
    .style("width", "100%")
    .attr("viewBox", [0, 0, width, height])
    .style("opacity", 0); 

    svg.transition()
    .duration(3000)
    .delay((d,i) => i*200)
    .style("opacity", 1)

  // Heatmap
  const gCategory = svg
    .selectAll(".category-g")
    .data(grouped, (d) => d.key)
    .join("g")
    .attr("class", "category-g")
    .attr("fill", (d) => color(d.key))
    .attr("transform", (_, i) => {
      const y = Math.floor(i / numCategoryCols);
      const x = i % numCategoryCols;
      return `translate(${gridSize * numEntryCols * x},${
        gridSize * numEntryRows * y
      })`;
    });
  const gEntry = gCategory
    .selectAll(".entry-g")
    .data((d) => d.values)
    .join("g")
    .attr("class", "entry-g")
    .attr("transform", (_, i) => {
      const y = Math.floor(i / numEntryCols);
      const x = i % numEntryCols;
      return `translate(${gridSize * x},${gridSize * y})`;
    });
  const entry = gEntry
    .append("rect")
    .attr("width", gridSize)
    .attr("height", gridSize)
    .attr("fill-opacity", (d) => d.Severity / 100)
    .on("mouseenter", showTooltip)
    .on("mouseleave", hideTooltip);
Related