Search code examples
d3.jshovertooltipclip-path

d3 hover text to clippath


I'm trying to add a tooltip on hover to a clip-path [this is my first time using clippath... is that even the right verbiage?]... I think I'm really close [the circle that should follow the line shows up at (0.0) for now] but I seem to be missing something. I was wondering if anyone has time to take a look? I'm trying to adapt code from https://blockbuilder.org/bendoesdata/2c8b315d103bbaf98264efda92d313ab

var data3 = [
  { group: 1, ser1: "2020-01-01", ser2: 3 },
  { group: 1, ser1: "2020-01-02", ser2: 5 },
  { group: 1, ser1: "2020-01-03", ser2: 9 },
  { group: 1, ser1: "2020-01-04", ser2: 3 },
  { group: 1, ser1: "2020-01-05", ser2: 5 },
  { group: 1, ser1: "2020-01-06", ser2: 9 },
  { group: 2, ser1: "2020-01-07", ser2: 10 },
  { group: 2, ser1: "2020-01-08", ser2: 9 },
  { group: 2, ser1: "2020-01-09", ser2: 10 },
  { group: 2, ser1: "2020-01-10", ser2: 20 },
  { group: 2, ser1: "2020-01-11", ser2: 10 },
  { group: 2, ser1: "2020-01-12", ser2: 12 },
  { group: 3, ser1: "2020-01-13", ser2: 20 },
  { group: 3, ser1: "2020-01-14", ser2: 12 },
  { group: 3, ser1: "2020-01-15", ser2: 4 },
  { group: 3, ser1: "2020-01-16", ser2: 22 },
  { group: 3, ser1: "2020-01-17", ser2: 2 },
  { group: 3, ser1: "2020-01-18", ser2: 4 },
]

var line = d3.line()
  .x(function (d) { return x(formatDate(d.ser1)); })
  .y(function (d) { return y(d.ser2); })

// set the dimensions and margins of the graph
var margin = { top: 10, right: 30, bottom: 30, left: 50 },
  width = 1000 - margin.left - margin.right,
  height = 400 - margin.top - margin.bottom;

// append the svg object to the body of the page
var svg = d3.select("#my_dataviz")
  .append("svg")
  .attr("viewBox", `0 0 ${width + margin.left + margin.right} ${height + margin.top + margin.bottom}`)
  .append("g")
  .attr("transform",
    "translate(" + margin.left + "," + margin.top + ")");

svg.append("defs")
  .append("clipPath")
  .attr("id", "chart-path")
  .append("rect")
  .attr("width", width - 100)
  .attr("height", height)

var formatDate = d3.timeParse("%Y-%m-%d");

var bisectDate = d3.bisector(function (d) {
  return d.str1;
}).left;

// Initialize a X axis:
var x = d3.scaleTime().range([0, width - 100]);
var xNum = d3.scaleLinear().range([0, width - 100])
var xAxis = d3.axisBottom().scale(x);
svg.append("g")
  .attr("transform", "translate(0," + height + ")")
  .attr("class", "myXaxis")

// Initialize an Y axis
var y = d3.scaleLinear().range([height, 0]);
var yAxis = d3.axisLeft().scale(y);
svg.append("g")
  .attr("class", "myYaxis")

// create the Y axis
y.domain([0, d3.max(data3, function (d) { return d.ser2 })]);
svg.selectAll(".myYaxis")
  .transition()
  .duration(1000)
  .call(yAxis);

// Create a update selection: bind to the new data
var u = svg.selectAll(".lineTest")
  .data([data3])
  .enter()
  .append("path")
  .attr("class", "lineTest")
  .attr("fill", "none")
  .attr("stroke", "#0b6dbd")
  .attr("stroke-width", 2.5)
  .attr("clip-path", "url(#chart-path)")


// Create a function that takes a dataset as input and update the plot:
function update(data) {
  // Create the X axis:
  x.domain(d3.extent(data, function (d) { return formatDate(d.ser1) }));
  svg.selectAll(".myXaxis")
    .transition()
    .duration(1000)
    .call(xAxis);
  u.transition()
    .duration(1000)
    .attr("d", line);
}

// At the beginning, I run the update function on the first dataset:
update(data3)


// this is where I try to implement a tooltip based on
// https://blockbuilder.org/bendoesdata/2c8b315d103bbaf98264efda92d313ab
function drawFocus() {

  // Create focus object
  let focus = svg.append('g')
    .attr('class', 'focus')

  // append circle on the line path
  focus.append('circle')
    .attr('r', 7.5)

  // add background rectangle behind the text tooltip
  focus.append('rect')
    .attr('x', -30)
    .attr('y', '-2em')
    .attr('width', 70)
    .attr('height', 20)
    .style("fill", "white");

  // add text annotation for tooltip
  focus.append('text')
    .attr('x', -30)
    .attr('dy', '-1em')
    .style("fill", "black")
    .style("font-family", "SuisseIntl");

  focus.append('div')
    .attr('x', 10)
    .attr('dy', '.35em')
    .attr("class", "tooltip")
    .style("opacity", 1)

  // create an overlay rectangle to draw the above objects on top of
  svg.append('rect')
    .attr('class', 'overlay')
    .attr('width', width - 100)
    .attr('height', height)
    .on('mouseover', () => focus.style('display', null))
    .on('mouseout', () => focus.style('display', 'none'))
    .on('mousemove', tipMove);

  // make the overlay rectangle transparent,
  // so it only serves the purpose of detecting mouse events
  d3.select('.overlay')
    .style('fill', 'none')
    .style('pointer-events', 'all');

  // select focus objects and set opacity
  d3.selectAll('.focus')
    .style('opacity', 0.9);

  // select the circle and style it
  d3.selectAll('.focus circle')
    .style("fill", '#FDD511')
    .style("opacity", 0)


  // function that adds tooltip on hover
  function tipMove() {
    // below code finds the date by bisecting and
    // stores the x and y coordinate as variables
    let x0 = x.invert(d3.mouse(this)[0]);
    let i = bisectDate(data3, x0, 1);
    let d0 = data3[i - 1];
    let d1 = data3[i];
    let d = x0 - d0.str1 > d1.str1 - x0 ? d1 : d0;

    // place the focus objects on the same path as the line
    focus.attr('transform', `translate(${x(d.date)}, ${y(d.value)})`);

    // position the x line
    focus.select('line.x')
      .attr('x1', 0)
      .attr('x2', x(formatDate(d.str1)))
      .attr('y1', 0)
      .attr('y2', 0);

    // position the y line
    focus.select('line.y')
      .attr('x1', 0)
      .attr('x2', 0)
      .attr('y1', 0)
      .attr('y2', height - y(d.str2));

    // position the text
    focus.select('text').text(d.str2).transition() // slowly fade in the tooltip
      .duration(100)
      .style("opacity", 1);

    // show the circle on the path
    focus.selectAll('.focus circle')
      .style("opacity", 1)

  };
}

drawFocus();
.tooltip {
    position: absolute;
    text-align: left;
    font-family: "Open Sans Condensed";
    font-size: 12px;
    width: 80px;
    height: 52px;
    padding: 8px;
    background: white;
    pointer-events: none;
    background-color: white;
    line-height: 0.05em;
    padding: 20px;
    /* border: solid;
        border-width: 1px; */
  }

  #my_dataviz {
    padding: 20px;
  }
<script src="https://d3js.org/d3.v5.js"></script>
<div id="my_dataviz"></div>


Solution

    1. You confused your naming. ser1 you called str1 a lot of times. This is a clear sign that your original naming ser1 and ser2 is not logical enough! If you can't follow it now, how about in a year when you revisit the code?

    2. Parse your data before using it, not when you use it. If you store a date as a string in the original object, parse it before doing stuff with it. Otherwise you'll run into problems when you want to compare, plot, format, or just use it.

    That's it, most changes I made were just to replace formatDate when needed with parseDate (these are different things) at the beginning, and replacing str1/2 with ser1/2 wherever needed.

    var data3 = [
      { group: 1, ser1: "2020-01-01", ser2: 3 },
      { group: 1, ser1: "2020-01-02", ser2: 5 },
      { group: 1, ser1: "2020-01-03", ser2: 9 },
      { group: 1, ser1: "2020-01-04", ser2: 3 },
      { group: 1, ser1: "2020-01-05", ser2: 5 },
      { group: 1, ser1: "2020-01-06", ser2: 9 },
      { group: 2, ser1: "2020-01-07", ser2: 10 },
      { group: 2, ser1: "2020-01-08", ser2: 9 },
      { group: 2, ser1: "2020-01-09", ser2: 10 },
      { group: 2, ser1: "2020-01-10", ser2: 20 },
      { group: 2, ser1: "2020-01-11", ser2: 10 },
      { group: 2, ser1: "2020-01-12", ser2: 12 },
      { group: 3, ser1: "2020-01-13", ser2: 20 },
      { group: 3, ser1: "2020-01-14", ser2: 12 },
      { group: 3, ser1: "2020-01-15", ser2: 4 },
      { group: 3, ser1: "2020-01-16", ser2: 22 },
      { group: 3, ser1: "2020-01-17", ser2: 2 },
      { group: 3, ser1: "2020-01-18", ser2: 4 },
    ];
    
    var parseDate = d3.timeParse("%Y-%m-%d");
    // Parse the date ASAP
    data3 = data3.map(function(d) {
      return {
        group: d.group,
        ser1: parseDate(d.ser1),
        ser2: d.ser2
      };
    });
    
    var line = d3.line()
      .x(function (d) { return x(d.ser1); })
      .y(function (d) { return y(d.ser2); })
    
    // set the dimensions and margins of the graph
    var margin = { top: 10, right: 30, bottom: 30, left: 50 },
      width = 1000 - margin.left - margin.right,
      height = 400 - margin.top - margin.bottom;
    
    // append the svg object to the body of the page
    var svg = d3.select("#my_dataviz")
      .append("svg")
      .attr("viewBox", `0 0 ${width + margin.left + margin.right} ${height + margin.top + margin.bottom}`)
      .append("g")
      .attr("transform",
        "translate(" + margin.left + "," + margin.top + ")");
    
    svg.append("defs")
      .append("clipPath")
      .attr("id", "chart-path")
      .append("rect")
      .attr("width", width - 100)
      .attr("height", height)
    
    var formatDate = d3.timeParse("%Y-%m-%d");
    
    var bisectDate = d3.bisector(function (d) {
      return d.ser1;
    }).left;
    
    // Initialize a X axis:
    var x = d3.scaleTime()
      .range([0, width - 100])
      .domain(d3.extent(data3, function (d) { return d.ser1; }));
    var xNum = d3.scaleLinear().range([0, width - 100])
    var xAxis = d3.axisBottom().scale(x);
    svg.append("g")
      .attr("transform", "translate(0," + height + ")")
      .attr("class", "myXaxis")
    
    // Initialize an Y axis
    var y = d3.scaleLinear().range([height, 0]);
    var yAxis = d3.axisLeft().scale(y);
    svg.append("g")
      .attr("class", "myYaxis")
    
    // create the Y axis
    y.domain([0, d3.max(data3, function (d) { return d.ser2 })]);
    svg.selectAll(".myYaxis")
      .transition()
      .duration(1000)
      .call(yAxis);
    
    // Create a update selection: bind to the new data
    var u = svg.selectAll(".lineTest")
      .data([data3])
      .enter()
      .append("path")
      .attr("class", "lineTest")
      .attr("fill", "none")
      .attr("stroke", "#0b6dbd")
      .attr("stroke-width", 2.5)
      .attr("clip-path", "url(#chart-path)")
    
    svg.selectAll(".myXaxis")
      .call(xAxis);
    u.attr("d", line);
    
    // this is where I try to implement a tooltip based on
    // https://blockbuilder.org/bendoesdata/2c8b315d103bbaf98264efda92d313ab
    function drawFocus() {
      // Create focus object
      let focus = svg.append('g')
        .attr('class', 'focus')
    
      // append circle on the line path
      focus.append('circle')
        .attr('r', 7.5)
    
      // add background rectangle behind the text tooltip
      focus.append('rect')
        .attr('x', -30)
        .attr('y', '-2em')
        .attr('width', 70)
        .attr('height', 20)
        .style("fill", "white");
    
      // add text annotation for tooltip
      focus.append('text')
        .attr('x', -30)
        .attr('dy', '-1em')
        .style("fill", "black")
        .style("font-family", "SuisseIntl");
    
      focus.append('div')
        .attr('x', 10)
        .attr('dy', '.35em')
        .attr("class", "tooltip")
        .style("opacity", 1)
    
      // create an overlay rectangle to draw the above objects on top of
      svg.append('rect')
        .attr('class', 'overlay')
        .attr('width', width - 100)
        .attr('height', height)
        .on('mouseover', () => focus.style('display', null))
        .on('mouseout', () => focus.style('display', 'none'))
        .on('mousemove', tipMove);
    
      // make the overlay rectangle transparent,
      // so it only serves the purpose of detecting mouse events
      d3.select('.overlay')
        .style('fill', 'none')
        .style('pointer-events', 'all');
    
      // select focus objects and set opacity
      d3.selectAll('.focus')
        .style('opacity', 0.9);
    
      // select the circle and style it
      d3.selectAll('.focus circle')
        .style("fill", '#FDD511')
        .style("opacity", 0)
    
    
      // function that adds tooltip on hover
      function tipMove() {
        // below code finds the date by bisecting and
        // stores the x and y coordinate as variables
        let x0 = x.invert(d3.mouse(this)[0]);
        let i = bisectDate(data3, x0, 1);
        let d0 = data3[i - 1];
        let d1 = data3[i];
        let d = x0 - d0.ser1 > d1.ser1 - x0 ? d1 : d0;
    
        // place the focus objects on the same path as the line
        focus.attr('transform', `translate(${x(d.ser1)}, ${y(d.ser2)})`);
    
        // position the x line
        focus.select('line.x')
          .attr('x1', 0)
          .attr('x2', x(d.ser1))
          .attr('y1', 0)
          .attr('y2', 0);
    
        // position the y line
        focus.select('line.y')
          .attr('x1', 0)
          .attr('x2', 0)
          .attr('y1', 0)
          .attr('y2', height - y(d.ser2));
    
        // position the text
        focus.select('text').text(d.ser2)
          .transition() // slowly fade in the tooltip
          .duration(100)
          .style("opacity", 1);
    
        // show the circle on the path
        focus.selectAll('.focus circle')
          .style("opacity", 1)
    
      };
    }
    
    drawFocus();
    .tooltip {
        position: absolute;
        text-align: left;
        font-family: "Open Sans Condensed";
        font-size: 12px;
        width: 80px;
        height: 52px;
        padding: 8px;
        background: white;
        pointer-events: none;
        background-color: white;
        line-height: 0.05em;
        padding: 20px;
        /* border: solid;
            border-width: 1px; */
      }
    
      #my_dataviz {
        padding: 20px;
      }
    <script src="https://d3js.org/d3.v5.js"></script>
    <div id="my_dataviz"></div>