javascriptd3.jstooltiplinegraph

Add html table with svg to tooltip of d3 treemap


I am doing a visualization web application that presents data using a d3 tree map.

When the user is hovering on the treemap, there is a tooltip displaying data as table with many rows and column like 5x4 for example.

I can display the data in every row correctly, but I have difficulty to display a line graph into one column of each row.

Below is the code that I use:

// Tooltip
leaf.on("mousemove", function (d) {
  if (d.depth < 3) return;
  
  var name1 = data.name;
  var name2 = d.parent.data.name;
  var name3 = d.data.name;
  var category = d.data.category;
  var change = d.data.change;
  var filtered = items.filter(function (item) { return item.level2 == d.parent.data.name });

  var tooltip_template = '\
    <h3 class="tooltip-header"> ' + name2 + ' / ' + name3 + '</h3> \
    <div class="tooltip-body"> \
    <table> \
  ';

  filtered.forEach(function (item) {
    tooltip_template += item.name == d.data.name
      ? '<tr style="color: #fff; background: ' + getColor(d.data.change) + '"> \
           <td> ' + item.name + '</td> \
           <td> \
             <svg></svg> \
           </td> \
           <td class="txtright"> ' + item.value + ' 건</td> \
           <td class="txtright"> ' + (item.change > 0 ? '+' : '') + item.change + ' 건</td> \
         </tr>'
      : '<tr> \
           <td> ' + item.name + '</td> \
           <td> \
             <svg></svg> \
           </td> \
           <td class="txtright"> ' + item.value + ' 건</td> \
           <td class="txtright"> ' + (item.change > 0 ? '+' : '') + item.change + ' 건</td> \
         </tr>';

    // set the dimensions and margins of the line graph
    var linemargin = {
      top: d3.event.pageY + 15,
      right: d3.event.pageX + 5,
      bottom: d3.event.pageY + 15,
      left: d3.event.pageX + 5
    }, linewidth = 100 - linemargin.left - linemargin.right, lineheight = 20 - linemargin.top - linemargin.bottom;

    // Set the data for the chart
    const linedata = [
      { x: 0, y: 5 },
      { x: 1, y: 9 },
      { x: 2, y: 7 },
      { x: 3, y: 5 },
      { x: 4, y: 3 },
      { x: 5, y: 4 },
      { x: 6, y: 8 },
      { x: 7, y: 6 },
      { x: 8, y: 3 },
      { x: 9, y: 2 },
    ];

    const linechart = tooltip.selectAll("svg").append("g").enter().attr("class", "linechart").attr("transform", `translate(${linemargin.left}, ${linemargin.top})`);

    const x = d3.scaleLinear().domain([0, 9]).range([0, linewidth]);
    const y = d3.scaleLinear().domain([0, 10]).range([lineheight, 0]);

    // Create the line generator
    const line = d3
      .line()
      .x((d) => x(d.x))
      .y((d) => y(d.y));

    // Add the line to the chart
    linechart.append("path").datum(linedata).attr("class", "line").attr("d", line);
  })

  tooltip_template += ' \
        </tbody> \
      </table> \
    </div> \
    '
    
  tooltip
    .attr('id', 'tooltip')
    .attr('data-change', change)
    .style("", "#")

  tooltip.transition()      
    .style("opacity", 1);

  tooltip.html(tooltip_template) 
    .style("left", (d3.event.pageX + 5) + "px")
    .style("top", (d3.event.pageY + 15) + "px")
});

Please help!


Solution

  • Potentially the core problem is that you create an <svg> in tooltip_template but then you never assign any dimensions to it (e.g. height and width).

    There are some additional adjustments you can make:

    With these adjustments you can rely on HTML rather than strings and move the computation of the table to an event handler called once rather than many times.

    Working example below with randomised content:

    const leaf = d3.selectAll(".leaf");
    
    leaf.on("mouseenter", function(d) {
    
      const table = d3.select("#custom-tooltip table")
    
      // remove any rows 
      table.selectAll("*").remove();
    
      // add random 2-5 data rows with content
      const rowCount = Math.floor(Math.random() * 4) + 2;
      
      for (let r=0; r<rowCount; ++r) {
        // add row
        const templateRow = document.querySelector("#tooltip-datarow").content.cloneNode(true);
        table.node().appendChild(templateRow);
        
        // add line chart
        const d3row = table.select("tr#datarow_x")
        d3row.attr("id", `datarow_${r}`);
        const linechart = d3row.select("svg.chartarea");
    
        linechart
          .append("path")
          .datum(linedata()) // randomised - replace with your data
          .attr("class", "line") 
          .attr("d", line);
    
        // add other details
        d3row.select(".label1").html(`Rand: ${Math.random().toFixed(2)}`);
        d3row.select(".label2").html(`Rand: ${Math.random().toFixed(2)}`);
        d3row.select(".label3").html(`Rand: ${Math.random().toFixed(2)}`);
    
        // show tooltip
        d3.select("#custom-tooltip")
          .style("visibility", "visible");
    
      }
    
        
    });
    
    leaf.on("mousemove", function(d) {
    
      // update tooltip with mouse 
      d3.select("#custom-tooltip")
        .style("top", (d3.event.pageY + 5) + "px")
        .style("left", (d3.event.pageX + 5) + "px")
    
    });
    
    leaf.on("mouseout", function(d) {
    
      // hide tooltip
      d3.selectAll("#custom-tooltip")
        .style("visibility", "hidden")
      
    });
    .leaf { width: 80px; height: 80px; fill: blue; }
    .chartarea { width: 100px; height: 20px; }
    .line { stroke: red; stroke-width: 1; fill: none; }
    #custom-tooltip { position: absolute; z-index: 10; visibility: hidden; }
    #custom-tooltip tr { background-color: #d6eeee; }
    #tooltip-header { background-color: #eeD600; }
    <script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
    
    <svg class="container">
      <rect class="leaf"></rect>
    </svg>
    
    <div id="custom-tooltip">
      <h3 id="tooltip-header">Header</h3>
      <div>
        <table>
        </table>  
      </div>
    </div>
    
    <template id="tooltip-datarow">
      <tr id="datarow_x">
        <td class="label1">Label</td>
        <td>
          <svg class="chartarea"></svg>
        </td>
        <td class="txtright label2">123</td>
        <td class="txtright label3">456</td>
      </tr>
    </template>
    
    <script>
    // random data for line
    function linedata() {
      return d3.range(10).map((_, i) => ({x: i, y: Math.floor(Math.random() * 10) + 1}));
    }
    
    // scales
    const x = d3.scaleLinear().range([0, 100]).domain([0, 9]);
    const y = d3.scaleLinear().range([0, 15]).domain([0, 9]);
    
    // line generator
    const line = d3
      .line()
      .x((d) => x(d.x))
      .y((d) => y(d.y));
         
    </script>