htoniv
htoniv

Reputation: 1668

Is it possible to add zoom and tooltip on the same line chart in d3js?

Currently I am learning d3js, one of the feature i like to implement is showing tooltip and zooming horizontally. I figured out how to add zooming in the chart (working fiddle) but feeling little complex in adding tooltip when hover over the points. Is it possible in d3js. Because when zooming we are adding rect element on the svg element. if we add the rect element in the chart means how to make this tooltip works. Need some help from d3 ninjas.

var data = [{
    date: "10:30:00",
    price: 36000
  },
  {
    date: "11:00:20",
    price: 40000
  },
  {
    date: "12:00:00",
    price: 38000
  },
  {
    date: "14:20:00",
    price: 50400
  }
];

var svg = d3.select("svg"),
  margin = {
    top: 20,
    right: 20,
    bottom: 110,
    left: 40
  },
  margin2 = {
    top: 430,
    right: 20,
    bottom: 30,
    left: 40
  },
  width = +svg.attr("width") - margin.left - margin.right,
  height = +svg.attr("height") - margin.top - margin.bottom,
  height2 = +svg.attr("height") - margin2.top - margin2.bottom;

var parseDate = d3.timeParse("%H:%M:%S"); //"%b %Y");

var x = d3.scaleTime().range([0, width]),
  x2 = d3.scaleTime().range([0, width]),
  y = d3.scaleLinear().range([height, 0]),
  y2 = d3.scaleLinear().range([height2, 0]);

var xAxis = d3.axisBottom(x),
  xAxis2 = d3.axisBottom(x2),
  yAxis = d3.axisLeft(y);

var brush = d3.brushX()
  .extent([
    [0, 0],
    [width, height2]
  ])
  .on("brush end", brushed);

var zoom = d3.zoom()
  .scaleExtent([1, Infinity])
  .translateExtent([
    [0, 0],
    [width, height]
  ])
  .extent([
    [0, 0],
    [width, height]
  ])
  .on("zoom", zoomed);

var area = d3.line()
  //.curve(d3.curveMonotoneX)
  .x(function(d) {
    return x(d.date);
  })
  .y(function(d) {
    return y(d.price);
  });

var area2 = d3.line()
  .curve(d3.curveMonotoneX)
  .x(function(d) {
    return x2(d.date);
  })
  .y(function(d) {
    return y2(d.price);
  });


svg.append("defs").append("clipPath")
  .attr("id", "clip")
  .append("rect")
  .attr("width", width)
  .attr("height", height);

var focus = svg.append("g")
  .attr("class", "focus")
  .attr("transform", "translate(" + margin.left + "," + margin.top + ")");

var context = svg.append("g")
  .attr("class", "context")
  .attr("transform", "translate(" + margin2.left + "," + margin2.top + ")");

function update() {

  for (var k in data) {
    type(data[k]);
  }

  x.domain(d3.extent(data, function(d) {
    return d.date;
  }));
  y.domain([0, d3.max(data, function(d) {
    return d.price;
  })]);
  x2.domain(x.domain());
  y2.domain(y.domain());


  focus.append("path")
    .datum(data)
    .attr("class", "area")
    .attr("d", area);

  focus.append("g")
    .attr("class", "axis axis--x")
    .attr("transform", "translate(0," + height + ")")
    .call(xAxis);

  focus.append("g")
    .attr("class", "axis axis--y")
    .call(yAxis);

  focus.selectAll("circle")
    .data(data)
    .enter().append("circle")
    .attr("class", "circle")
    .attr("r", 5)
    .style("fill", 'orange')
    .style("stroke", 'red')
    .style("stroke-width", "2")
    .attr("cx", function(d) {
      return x(d.date)
    })
    .attr("cy", function(d) {
      return y(d.price);
    });

  context.append("path")
    .datum(data)
    .attr("class", "area")
    .attr("d", area2);

  context.append("g")
    .attr("class", "axis axis--x")
    .attr("transform", "translate(0," + height2 + ")")
    .call(xAxis2);

  context.selectAll("circle")
    .data(data)
    .enter().append("circle")
    .attr("class", "circle")
    .attr("r", 1)
    .style("fill", 'blue')
    .style("stroke", 'red')
    .style("stroke-width", "2")
    .attr("cx", function(d) {
      return x(d.date)
    })
    .attr("cy", function(d) {
      return y(d.price);
    });

  context.append("g")
    .attr("class", "brush")
    .call(brush)
    .call(brush.move, x.range());

  svg.append("rect")
    .attr("class", "zoom")
    .attr("width", width)
    .attr("height", height)
    .attr("transform", "translate(" + margin.left + "," + margin.top + ")")
    .call(zoom);
}



function brushed() {
  if (d3.event.sourceEvent && d3.event.sourceEvent.type === "zoom") return; // ignore brush-by-zoom
  var s = d3.event.selection || x2.range();
  x.domain(s.map(x2.invert, x2));
  focus.select(".area").attr("d", area);

  focus.selectAll('.circle')
    .attr("cx", function(d) {
      return x(d.date)
    })
    .attr("cy", function(d) {
      return y(d.price);
    });



  focus.select(".axis--x").call(xAxis);
  svg.select(".zoom").call(zoom.transform, d3.zoomIdentity
    .scale(width / (s[1] - s[0]))
    .translate(-s[0], 0));
}

function zoomed() {
  if (d3.event.sourceEvent && d3.event.sourceEvent.type === "brush") return; // ignore zoom-by-brush
  var t = d3.event.transform;
  x.domain(t.rescaleX(x2).domain());
  focus.select(".area").attr("d", area);

  focus.selectAll('.circle')
    .attr("cx", function(d) {
      return x(d.date)
    })
    .attr("cy", function(d) {
      return y(d.price);
    });

  focus.select(".axis--x").call(xAxis);
  context.select(".brush").call(brush.move, x.range().map(t.invertX, t));
  context.selectAll('.circle')
    .attr("cx", function(d) {
      return x(d.date)
    })
    .attr("cy", function(d) {
      return y(d.price);
    });
}

function type(d) {
  d.date = parseDate(d.date);
  d.price = +d.price;
  return d;
}
update();
.area {
  fill: none;
  stroke: #a2dced;
  stroke-width: 2;
  clip-path: url(#clip);
}

.zoom {
  cursor: move;
  fill: none;
  pointer-events: all;
}

rect.selection {
  fill: green;
}
<script src="https://d3js.org/d3.v4.min.js"></script>
<svg width="960" height="500"></svg>

Upvotes: 2

Views: 1551

Answers (1)

Ruben Helsloot
Ruben Helsloot

Reputation: 13139

Of course it's possible to add a tooltip in d3, there are a lot of examples and even a dedicated package for older versions.

You can choose to show a tooltip inside the SVG (as a rect with text) or outside, as a div. The benefit of outside is that the tooltip can overflow the SVG, the downside is that positioning can be more difficult, especially with scrolling.

I show a very simple implementation below, using a DIV tooltip. I positioned the .zoom rect behind the circles, so they would catch the mouse events instead, and added on mouseenter and mouseleave event listeners.

var data = [{
    date: "10:30:00",
    price: 36000
  },
  {
    date: "11:00:20",
    price: 40000
  },
  {
    date: "12:00:00",
    price: 38000
  },
  {
    date: "14:20:00",
    price: 50400
  }
];

var svg = d3.select("svg"),
  margin = {
    top: 20,
    right: 20,
    bottom: 110,
    left: 40
  },
  margin2 = {
    top: 430,
    right: 20,
    bottom: 30,
    left: 40
  },
  width = +svg.attr("width") - margin.left - margin.right,
  height = +svg.attr("height") - margin.top - margin.bottom,
  height2 = +svg.attr("height") - margin2.top - margin2.bottom;

var parseDate = d3.timeParse("%H:%M:%S"); //"%b %Y");

var x = d3.scaleTime().range([0, width]),
  x2 = d3.scaleTime().range([0, width]),
  y = d3.scaleLinear().range([height, 0]),
  y2 = d3.scaleLinear().range([height2, 0]);

var xAxis = d3.axisBottom(x),
  xAxis2 = d3.axisBottom(x2),
  yAxis = d3.axisLeft(y);

var brush = d3.brushX()
  .extent([
    [0, 0],
    [width, height2]
  ])
  .on("brush end", brushed);

var zoom = d3.zoom()
  .scaleExtent([1, Infinity])
  .translateExtent([
    [0, 0],
    [width, height]
  ])
  .extent([
    [0, 0],
    [width, height]
  ])
  .on("zoom", zoomed);

var area = d3.line()
  //.curve(d3.curveMonotoneX)
  .x(function(d) {
    return x(d.date);
  })
  .y(function(d) {
    return y(d.price);
  });

var area2 = d3.line()
  .curve(d3.curveMonotoneX)
  .x(function(d) {
    return x2(d.date);
  })
  .y(function(d) {
    return y2(d.price);
  });


svg.append("defs").append("clipPath")
  .attr("id", "clip")
  .append("rect")
  .attr("width", width)
  .attr("height", height);

var focus = svg.append("g")
  .attr("class", "focus")
  .attr("transform", "translate(" + margin.left + "," + margin.top + ")");

var context = svg.append("g")
  .attr("class", "context")
  .attr("transform", "translate(" + margin2.left + "," + margin2.top + ")");

var tooltip = d3.select('body')
  .append('div')
  .attr('id', 'tooltip')
  .style("transform", "translate(" + margin.left + "px," + margin.top + "px)")
  .classed('hide', true);

function update() {

  for (var k in data) {
    type(data[k]);
  }

  x.domain(d3.extent(data, function(d) {
    return d.date;
  }));
  y.domain([0, d3.max(data, function(d) {
    return d.price;
  })]);
  x2.domain(x.domain());
  y2.domain(y.domain());


  focus.append("path")
    .datum(data)
    .attr("class", "area")
    .attr("d", area);

  focus.append("g")
    .attr("class", "axis axis--x")
    .attr("transform", "translate(0," + height + ")")
    .call(xAxis);

  focus.append("g")
    .attr("class", "axis axis--y")
    .call(yAxis);

  focus.selectAll("circle")
    .data(data)
    .enter()
    .append("circle")
    .attr("class", "circle")
    .attr("r", 5)
    .style("fill", 'orange')
    .style("stroke", 'red')
    .style("stroke-width", "2")
    .attr("cx", function(d) {
      return x(d.date)
    })
    .attr("cy", function(d) {
      return y(d.price);
    })
    .on("mouseenter", function(d) {
      // Show the tooltip and position it correctly
      tooltip.classed('hide', false)
        .style('left', x(d.date).toString() + 'px')
        .style('top', y(d.price).toString() + 'px')
        .html("<p>Price: " + d.price + "</p>");
    })
    .on("mouseleave", function() {
      tooltip.classed('hide', true);
    });

  context.append("path")
    .datum(data)
    .attr("class", "area")
    .attr("d", area2);

  context.append("g")
    .attr("class", "axis axis--x")
    .attr("transform", "translate(0," + height2 + ")")
    .call(xAxis2);

  context.selectAll("circle")
    .data(data)
    .enter().append("circle")
    .attr("class", "circle")
    .attr("r", 1)
    .style("fill", 'blue')
    .style("stroke", 'red')
    .style("stroke-width", "2")
    .attr("cx", function(d) {
      return x(d.date)
    })
    .attr("cy", function(d) {
      return y(d.price);
    });

  context.append("g")
    .attr("class", "brush")
    .call(brush)
    .call(brush.move, x.range());

  // Insert the zoom rect *before* the circles, so the circles
  // are drawn in front of the recrt
  focus.insert("rect", "circle")
    .attr("class", "zoom")
    .attr("width", width)
    .attr("height", height)
    .call(zoom);
}



function brushed() {
  if (d3.event.sourceEvent && d3.event.sourceEvent.type === "zoom") return; // ignore brush-by-zoom
  var s = d3.event.selection || x2.range();
  x.domain(s.map(x2.invert, x2));
  focus.select(".area").attr("d", area);

  focus.selectAll('.circle')
    .attr("cx", function(d) {
      return x(d.date)
    })
    .attr("cy", function(d) {
      return y(d.price);
    });

  focus.select(".axis--x").call(xAxis);
  svg.select(".zoom").call(zoom.transform, d3.zoomIdentity
    .scale(width / (s[1] - s[0]))
    .translate(-s[0], 0));
}

function zoomed() {
  if (d3.event.sourceEvent && d3.event.sourceEvent.type === "brush") return; // ignore zoom-by-brush
  var t = d3.event.transform;
  x.domain(t.rescaleX(x2).domain());
  focus.select(".area").attr("d", area);

  focus.selectAll('.circle')
    .attr("cx", function(d) {
      return x(d.date)
    })
    .attr("cy", function(d) {
      return y(d.price);
    });

  focus.select(".axis--x").call(xAxis);
  context.select(".brush").call(brush.move, x.range().map(t.invertX, t));
  context.selectAll('.circle')
    .attr("cx", function(d) {
      return x(d.date)
    })
    .attr("cy", function(d) {
      return y(d.price);
    });
}

function type(d) {
  d.date = parseDate(d.date);
  d.price = +d.price;
  return d;
}
update();
.area {
  fill: none;
  stroke: #a2dced;
  stroke-width: 2;
  clip-path: url(#clip);
}

.zoom {
  cursor: move;
  fill: none;
  pointer-events: all;
}

rect.selection {
  fill: green;
}

#tooltip {
  position: absolute;
  border: solid 1px black;
  background: white;
  margin: 20px;
}

.hide {
  opacity: 0;
}
<script src="https://d3js.org/d3.v4.min.js"></script>
<svg width="960" height="500"></svg>

Upvotes: 3

Related Questions