问题
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>
回答1:
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>
来源:https://stackoverflow.com/questions/63879042/is-it-possible-to-add-zoom-and-tooltip-on-the-same-line-chart-in-d3js