Search code examples
d3.jssvgresponsive-designtooltipviewbox

D3, tooltip position, when viewBox is applied


I am creating pie chart.

When hovering segments, I want to display custom html tooltip ...

All the time from the beginning, I was using d3.event.pageX and d3.event.pageY for calculating tooltip locations.

But now I want to display tooltip in the center point of pie segment

enter image description here

So, I am calculating centroid on hover

 var centroid = arcs.pie.centroid(d);
            var left = centroid[0];
            var top = centroid[1];

and assign tooltip's left , right position accordingly

tooltip.style("left", (left) + "px")
       .style("top", (top) + "px");

That only works , when the chart size is initial, but I also want the chart to be responsive

so, I have assigned viewbox property on svg

 var svg = d3.select(selector)
        .append('svg')
        .attr("viewBox", "0 0 " + attrs.svgWidth + " " + attrs.svgHeight)
        .attr("preserveAspectRatio", "xMidYMid meet")

Here comes the problem:

When resizing window, chart scales accordingly, but tooltip position is out of segment center.

How can I move this tooltip to the right position?

I am looking "pluginless" solution.

check out codepen


Solution

  • Couple calculations needed here. First, after you get your centroid, you need to un-translate it since they are in a g transformation (and your div needs to orientate to top/left). Then you can simply get the current/actual width/height of the svg and scale the position by it's ratio to initial non-viewboxed width/height.

    var centroid = arcs.pie.centroid(d),
        svgDim = svg.node().getBoundingClientRect();
    
    var left = (centroid[0] + dynamic.marginLeft) * (svgDim.width/attrs.svgWidth),
        top = (centroid[1] + dynamic.marginTop) * (svgDim.height/attrs.svgHeight);
    

    Here's the full running code:

    function packData(p) {
      var data = p;
      var result = {
        title: data.datasets[0].label,
        data: []
      };
    
      data.labels.forEach(function(v) {
        result.data.push({
          label: v
        });
      });
    
      result.data.forEach(function(v, i) {
        v.value = data.datasets[0].data[i];
    
        if (data.datasets[0].backgroundColor) {
          v.backgroundColor = data.datasets[0].backgroundColor[i];
        }
    
        if (data.extras) {
          for (var attrname in data.extras[i]) {
            v[attrname] = data.extras[i][attrname];
          }
        }
      });
    
      return result;
    
    }
    
    var svg;
    
    function drawD3JsPie(selector) {
    
      var info = packData({
        labels: ["Niger", "Cameroon", "Georgia", "Spain", "United States", "Singapore", "Qatar"],
        datasets: [{
          label: 'GDP   Per Capita in $ (2015)',
          data: [1080, 3144, 9630, 34819, 55805, 85253, 132099],
          backgroundColor: ["#FF6384", "#36A2EB", "#FFCE56", "#AA6384", "#2CA21B", "#678E86", "#FFAE86"]
        }]
      });
      var data = info.data;
      // ########   hard coded and dynamically calculated attributes  #######
    
      var attrs = {
        svgWidth: 600,
        svgHeight: 600,
        marginLeft: 4,
        marginBottom: 20,
        marginRight: 4,
        marginTop: 20,
        textColor: '#7f7777',
        fontSize: '13px',
        pieStroke: 'white',
        pieStrokeWidth: 3,
        titleText: info.title,
        hoverColorImpact: 1,
        animationDuration: 1200,
        animationEase: 'out',
        titleHeight: 30,
        tooltipTextColor: 'white',
        tooltipBackgroundColor: 'black',
    
      }
    
      var dynamic = {}
      dynamic.chartWidth = attrs.svgWidth - attrs.marginLeft - attrs.marginRight
      dynamic.chartHeight = attrs.svgHeight - attrs.marginTop - attrs.marginBottom - attrs.titleHeight;
      dynamic.pieOuterRadius = Math.min(dynamic.chartWidth, dynamic.chartHeight) / 2;
      dynamic.chartTopMargin = attrs.marginTop + attrs.titleHeight;
      dynamic.marginLeft = attrs.marginLeft + dynamic.pieOuterRadius;
      dynamic.marginTop = dynamic.chartTopMargin + dynamic.pieOuterRadius;
    
      //  ##############     SCALES     #########
    
    
      //  ##############   ARCS   ###############
      var arcs = {}
      arcs.pie = d3.arc()
        .outerRadius(dynamic.pieOuterRadius - 10)
        .innerRadius(0);
    
    
      //  ##########     layouts  #######
      var layouts = {};
      layouts.pie = d3.pie()
        .sort(null)
        .value(function(d) {
          return d.value;
        });
    
    
      //###############  STARTUP ANIMATIONS ###############
      var tweens = {}
    
      tweens.pieIn = function(endData) {
        var startData = {
          startAngle: 0,
          endAngle: 0
        };
        var interpolation = d3.interpolate(startData, endData);
        return function(currentData) {
          return arcs.pie(interpolation(currentData));
        }
      };
    
    
      //  ########### RESPONSIVE SVG DRAWING  ##############
      svg = d3.select(selector)
        .append('svg')
        .attr("viewBox", "0 0 " + attrs.svgWidth + " " + attrs.svgHeight)
        .attr("preserveAspectRatio", "xMidYMid meet")
    
    
    
      //   #################  CHART CONTENT DRAWING  ###############
    
      var chart = svg.append('g')
        .attr('width', dynamic.chartWidth)
        .attr('height', dynamic.chartHeight)
        .attr('transform', 'translate(' + (dynamic.marginLeft) + ',' + (dynamic.marginTop) + ')');
    
    
      var pieArcs = chart.selectAll('.arc')
        .data(layouts.pie(data))
        .enter()
        .append("g")
        .attr("class", "arc");
    
    
      pieArcs.append("path")
        //.attr("d", arcs.pie)
        .attr('stroke-width', attrs.pieStrokeWidth)
        .attr('stroke', attrs.pieStroke)
        .attr("fill", function(d) {
          return d.data.backgroundColor;
        })
        .transition()
        .duration(1000)
        .attrTween("d", tweens.pieIn);
    
    
    
    
    
    
      //  ################   ADDING TOOLTIP ##################
      var div = d3.select(selector)
        .append("div")
        .attr("class", "tooltip")
        .style("opacity", 0)
        .style("position", 'absolute')
        .style("text-align", 'left')
        .style("font", '12px sans-serif')
        .style("background", attrs.tooltipBackgroundColor)
        .style("padding", '5px')
        .style("color", attrs.tooltipTextColor)
        .style("border", '0px')
        .style("border-radius", '4px')
        .style("pointer-events", 'none')
    
      d3.selectAll('.arc')
        .on("mouseover", function(d, i) {
          var centroid = arcs.pie.centroid(d),
            svgDim = svg.node().getBoundingClientRect();
    
          var left = (centroid[0] + dynamic.marginLeft) * (svgDim.width / attrs.svgWidth),
            top = (centroid[1] + dynamic.marginTop) * (svgDim.height / attrs.svgHeight);
    
          d3.select(this)
            .append('circle')
            .style('fill', 'steelblue')
            .attr("transform", "translate(" + centroid + ")")
            .attr("r", 10);
    
          var buffer = d.value.toString().length;
          if (i > data.length / 2) {
            buffer = -buffer - 180;
          } else {
            buffer *= 4;
          }
          div.transition()
            .duration(100)
            .style("opacity", .9);
    
          div.html("<b>" + attrs.titleText + "</b><br/>" + d.data.label + ' : ' + d.data.value)
            .style("left", (left) + "px")
            .style("top", (top) + "px");
    
          var currPath = d3.select(this).select('path');
          var darkenedColor = d3.rgb(currPath.attr('fill')).darker(attrs.hoverColorImpact);
          currPath.attr('fill', darkenedColor);
    
    
        })
        .on("mouseout", function(d) {
    
    
          div.transition()
            .duration(200)
            .style("opacity", 0);
    
          var currPath = d3.select(this).select('path');
          var changedColor = d3.rgb(currPath.attr('fill')).darker(-attrs.hoverColorImpact);
          currPath.attr('fill', changedColor);
    
        });
    
    
    
    
    }
    
    drawD3JsPie("#d3JsPie");
    <script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.2.2/d3.min.js"></script>
    <div style='height:50%;width:50%'id="d3JsPie"></div>