gty1996
gty1996

Reputation: 333

Adding a tooltip over different axis where the line goes through in d3

I want to add a tooltip showing the exact values when hovering over one of the lines in the plot. How can I add a tooltip on each of the axis where the line touches the axis when that line is hovered? Any help would be highly appreciated. Ex. The attributes first, second, third and fourth should be shown when hovering over car line. The same should happen for both test and train datasets.

var dataSet = [{
    "type": "car",
    "dataset": "test",
    "first": 0.65,
    "second": 0.34,
    "third": 0.55,
    "fourth": 0.39
  },
  {
    "type": "car",
    "dataset": "train",
    "first": 0.59,
    "second": 0.33,
    "third": 0.50,
    "fourth": 0.40
  },
  {
    "type": "bicycle",
    "dataset": "test",
    "first": 200,
    "second": 230,
    "third": 250,
    "fourth": 300
  },
  {
    "type": "bicycle",
    "dataset": "train",
    "first": 200,
    "second": 280,
    "third": 225,
    "fourth": 278
  },
  {
    "type": "boat",
    "dataset": "test",
    "first": 320,
    "second": 324,
    "third": 532,
    "fourth": 321
  },
  {
    "type": "boat",
    "dataset": "train",
    "first": 128,
    "second": 179,
    "third": 166,
    "fourth": 234
  },
  {
    "type": "airplane",
    "dataset": "test",
    "first": 1500,
    "second": 2000,
    "third": 2321,
    "fourth": 1793
  },
  {
    "type": "airplane",
    "dataset": "train",
    "first": 1438,
    "second": 2933,
    "third": 2203,
    "fourth": 2000
  }
];

var processedData = [];
dataSet.forEach(function(d) {
  var match = processedData.find(function(p) { return p.type === d.type; });
  if(!match) {
    match = {
      type: d.type,
    };
    processedData.push(match);
  }

  var values = [d.first, d.second, d.third, d.fourth];
  if(d.dataset === "train") {
    match.train = values;
  } else {
    match.test = values;
  }
});

processedData.forEach(function(d) {
  // Normalise the values in the arrays
  const min = Math.min(d3.min(d.train), d3.min(d.test));
  const max = Math.max(d3.max(d.train), d3.max(d.test));
  
  d.trainNormalised = d.train.map(function(v) {
    return (v - min) / (max - min);
  });
  d.testNormalised = d.test.map(function(v) {
    return (v - min) / (max - min);
  });
});

var margin = {
    top: 5,
    right: 50,
    bottom: 5,
    left: 70
  },
  width = 600 - margin.left - margin.right,
  height = 280 - margin.top - margin.bottom;

var categoryScale = d3.scale.ordinal()
  .domain(processedData.map(function(d) { return d.type; }))
  .rangePoints([0, height]);
var y = d3.scale.linear()
  .domain([0, 1])
  .range([height, 0]);

var x = d3.scale.ordinal()
  .domain(d3.range(5))
  .rangePoints([0, width]);

var line = d3.svg.line()
  .defined(function(d) {
    return !isNaN(d[1]);
  });

// CREATE A COLOR SCALE
var color = d3.scale.ordinal()
  .range(["#4683b8", "#79add2", "#a6c9de", "#cadbed", "#9d9bc4", "#bcbed9", "#dadaea", "#f6d2a8", "#f2b076", "#ef914e", "#d65e2a"])

var svg = d3.select("#parallel_coor")
  .append("svg")
  .attr("width", width + margin.left + margin.right)
  .attr("height", height + margin.top + margin.bottom)
  .append("g")
  .attr("transform",
    "translate(" + margin.left + "," + margin.top + ")");

svg.selectAll(".dimension.axis")
  .data([categoryScale, y, y, y, y])
  .enter()
  .append("g")
  .attr("class", "dimension axis")
  .attr("transform", function(d, i) {
    return "translate(" + x(i) + ")";
  })
  .each(function(d) {
    const yAxis = d3.svg.axis()
      .scale(d)
      .ticks([])
      .orient("left");
    d3.select(this).call(yAxis);
  });

function parallel(data) {
  // Draw one line group per type (car, boat)
  // Each line group consists of a train and a test line;
  var lineGroup = svg.append("g")
    .selectAll(".lineGroup")
    .data(data)
    .enter()
    .append("g")
    .attr("class", "lineGroup")
    .each(function(d) {
      if(d.train)
        d3.select(this).append("path")
          .datum([d, "train"]);

      if(d.test)
        d3.select(this).append("path")
          .datum(function(d) { return [d, "test"]; });
    })

  lineGroup
    .attr("stroke", function(d) {
      var company = d.type.slice(0, d.type.indexOf(' '));
      return color(company);
    })
    .selectAll("path")
    .attr("class", function(d) { return d[1]; })
    .attr("d", draw);
  
  lineGroup
    .on("mouseover", function(d) {
      // show train when click others
      d3.select(this).classed("active", true);
      lineGroup
        .filter(function(e) { return e.type !== d.type; })
        .style('opacity', 0.2);
    })
    .on("mouseout", function(d) {
      d3.select(this).classed("active", false);
      lineGroup.style('opacity', null);
    });

  function draw(d) {
    var data = d[0], type = d[1];
    var points = data[type + "Normalised"].map(function(v, i) {
      return [x(i + 1), y(v)];
    });
    points.unshift([x(0), categoryScale(data.type)]);
    return line(points);
  }
}

parallel(processedData);
svg {
  font: 12px sans-serif;
}

.lineGroup path {
  fill: none;
}

.lineGroup.active .train {
  visibility: visible;
}

.train {
  visibility: hidden;
  stroke-dasharray: 5 5;
}

.axis line,
.axis path {
  fill: none;
  stroke: #000;
  shape-rendering: crispEdges;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.17/d3.min.js"></script>
<div id="parallel_coor"></div>

Upvotes: 2

Views: 62

Answers (1)

Ruben Helsloot
Ruben Helsloot

Reputation: 13129

You could do something like the following. Whenever you hover over a line, draw rectangles for each data point, add some text and styling, and you're done. I've given the tooltips different classes so they can be styled based on whether they're for train or test data

var dataSet = [{
    "type": "car",
    "dataset": "test",
    "first": 0.65,
    "second": 0.34,
    "third": 0.55,
    "fourth": 0.39
  },
  {
    "type": "car",
    "dataset": "train",
    "first": 0.59,
    "second": 0.33,
    "third": 0.50,
    "fourth": 0.40
  },
  {
    "type": "bicycle",
    "dataset": "test",
    "first": 200,
    "second": 230,
    "third": 250,
    "fourth": 300
  },
  {
    "type": "bicycle",
    "dataset": "train",
    "first": 200,
    "second": 280,
    "third": 225,
    "fourth": 278
  },
  {
    "type": "boat",
    "dataset": "test",
    "first": 320,
    "second": 324,
    "third": 532,
    "fourth": 321
  },
  {
    "type": "boat",
    "dataset": "train",
    "first": 128,
    "second": 179,
    "third": 166,
    "fourth": 234
  },
  {
    "type": "airplane",
    "dataset": "test",
    "first": 1500,
    "second": 2000,
    "third": 2321,
    "fourth": 1793
  },
  {
    "type": "airplane",
    "dataset": "train",
    "first": 1438,
    "second": 2933,
    "third": 2203,
    "fourth": 2000
  }
];

var processedData = [];
dataSet.forEach(function(d) {
  var match = processedData.find(function(p) { return p.type === d.type; });
  if(!match) {
    match = {
      type: d.type,
    };
    processedData.push(match);
  }

  var values = [d.first, d.second, d.third, d.fourth];
  if(d.dataset === "train") {
    match.train = values;
  } else {
    match.test = values;
  }
});

processedData.forEach(function(d) {
  // Normalise the values in the arrays
  const min = Math.min(d3.min(d.train), d3.min(d.test));
  const max = Math.max(d3.max(d.train), d3.max(d.test));
  
  d.trainNormalised = d.train.map(function(v) {
    return (v - min) / (max - min);
  });
  d.testNormalised = d.test.map(function(v) {
    return (v - min) / (max - min);
  });
});

var margin = {
    top: 5,
    right: 50,
    bottom: 5,
    left: 70
  },
  width = 600 - margin.left - margin.right,
  height = 280 - margin.top - margin.bottom;

var categoryScale = d3.scale.ordinal()
  .domain(processedData.map(function(d) { return d.type; }))
  .rangePoints([0, height]);
var y = d3.scale.linear()
  .domain([0, 1])
  .range([height, 0]);

var x = d3.scale.ordinal()
  .domain(d3.range(5))
  .rangePoints([0, width]);

var line = d3.svg.line()
  .defined(function(d) {
    return !isNaN(d[1]);
  });

// CREATE A COLOR SCALE
var color = d3.scale.ordinal()
  .range(["#4683b8", "#79add2", "#a6c9de", "#cadbed", "#9d9bc4", "#bcbed9", "#dadaea", "#f6d2a8", "#f2b076", "#ef914e", "#d65e2a"])

var svg = d3.select("#parallel_coor")
  .append("svg")
  .attr("width", width + margin.left + margin.right)
  .attr("height", height + margin.top + margin.bottom)
  .append("g")
  .attr("transform",
    "translate(" + margin.left + "," + margin.top + ")");

svg.selectAll(".dimension.axis")
  .data([categoryScale, y, y, y, y])
  .enter()
  .append("g")
  .attr("class", "dimension axis")
  .attr("transform", function(d, i) {
    return "translate(" + x(i) + ")";
  })
  .each(function(d) {
    const yAxis = d3.svg.axis()
      .scale(d)
      .ticks([])
      .orient("left");
    d3.select(this).call(yAxis);
  });

function parallel(data) {
  // Draw one line group per type (car, boat)
  // Each line group consists of a train and a test line;
  var lineGroup = svg.append("g")
    .selectAll(".lineGroup")
    .data(data)
    .enter()
    .append("g")
    .attr("class", "lineGroup")
    .each(function(d) {
      if(d.train)
        d3.select(this).append("path")
          .datum([d, "train"]);

      if(d.test)
        d3.select(this).append("path")
          .datum(function(d) { return [d, "test"]; });
    })

  lineGroup
    .attr("stroke", function(d) {
      var company = d.type.slice(0, d.type.indexOf(' '));
      return color(company);
    })
    .selectAll("path")
    .attr("class", function(d) { return d[1]; })
    .attr("d", draw);
  
  lineGroup
    .on("mouseover", function(d) {
      // show train when click others
      d3.select(this).classed("active", true);
      lineGroup
        .filter(function(e) { return e.type !== d.type; })
        .style('opacity', 0.2);

      // add tooltip elements for all values
      drawTips(d, "train");
      drawTips(d, "test");
    })
    .on("mouseout", function(d) {
      d3.select(this).classed("active", false);
      lineGroup.style('opacity', null);
      
      svg.selectAll(".tooltip").remove();
    });

  function draw(d) {
    var data = d[0], type = d[1];
    var points = data[type + "Normalised"].map(function(v, i) {
      return [x(i + 1), y(v)];
    });
    points.unshift([x(0), categoryScale(data.type)]);
    return line(points);
  }
  
  function drawTips(data, type) {
    const newTips = svg.selectAll(".tooltip.tooltip-" + type)
      .data(data[type])
      .enter()
      .append("g")
      .attr("class", "tooltip tooltip-" + type)
      .attr("transform", function(d, i) {
        const v = data[type + "Normalised"][i];
        return "translate(" + [x(i +1), y(v)] + ")";
      });

    newTips.append("rect")
      .attr("width", 40)
      .attr("height", 16);
    
    newTips.append("text")
      .attr("dx", 4)
      .attr("dy", 8)
      .text(function(d) { return d; });
  }
}

parallel(processedData);
svg {
  font: 12px sans-serif;
}

.lineGroup path {
  fill: none;
}

.lineGroup.active .train {
  visibility: visible;
}

.train {
  visibility: hidden;
  stroke-dasharray: 5 5;
}

.axis line,
.axis path {
  fill: none;
  stroke: #000;
  shape-rendering: crispEdges;
}

.tooltip-test rect {
  fill: #444;
}

.tooltip-train rect {
  fill: #888;
}

.tooltip text {
  dominant-baseline: middle;
  fill: white;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.17/d3.js"></script>
<div id="parallel_coor"></div>

Upvotes: 1

Related Questions