2

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>
gty1996
  • 333
  • 1
  • 13
  • Does this answer your question? [Show data on mouseover of circle](https://stackoverflow.com/questions/10805184/show-data-on-mouseover-of-circle) – Ruben Helsloot Nov 13 '20 at 16:22
  • @RubenHelsloot thanks. But I know how to add a tooltip, the problem is I can't seem to add them to all of the attributes simultaneously. So that when I hover over one of the lines, the values of each point of that line over the different axis to show. – gty1996 Nov 13 '20 at 16:31

1 Answers1

1

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>
Ruben Helsloot
  • 12,582
  • 6
  • 26
  • 49
  • this .data(data[type]) is how you get the data for each type and then I don't understand why you add + "Normalised" in `const v`. Could you explain how you did it please? So that I can learn it better. Thanks so much! – gty1996 Nov 13 '20 at 17:50
  • 1
    `data` has the property `train` (0, 40, 35, 50) and the property `trainNormalised` (0, 0.8, 0.7, 1). The former has the correct values, but the position is based on the normalised ones - because of your previous question. So you need `trainNormalised` to position `y` and `train` to fill the `text` node – Ruben Helsloot Nov 13 '20 at 17:54