实时代码:https://blockbuilder.org/ashleighc207/ef14357436ffe981f1cd5a841b8a8558
我有一个图表库,里面充满了各种D3.js图表(折线图,条形图,地图等),它们都使用一个函数来创建其工具提示。工具提示创建为svg多边形(特别是为了支持IE,是的,我必须这样做)。我目前正在将宽度和高度参数传递到基于图形进行硬编码的工具提示函数中。
这不是超级动态的,当您使用更长的单词/更大的数字时会成为一个问题。我想根据文本元素的边界框(或BBox)来确定宽度和高度。我在网上遇到了一些解决方案,建议创建工具提示,附加文本,然后再调整工具提示的宽度/高度。
我发现上述方法有两个问题-首先,当在 之前添加空文本元素时,Safari弄乱了我的其他BBox计算。其次-因为我使用的是多边形,所以它不像重新设置宽度和高度那样简单。我必须重新绘制多边形路径。
TLDR;我正在寻找一种基于内部文本动态调整工具提示大小的最佳方法,即使在生成文本元素之前使用width / height参数调用了工具提示功能。
这是工具提示代码:(我知道在重构过程中有点混乱,这起着至关重要的作用)
function createTooltip(element, elementName, width, height, calloutDirection, secondData) {
// check if tooltip exists, and if not, continue
if (d3.select(element + " #" + elementName + "-tooltip-container").empty() !== false) {
// set variables for width and height difference - exists because tooltip is made to be 135 x 50 by default
var widthDiff = 135 - width,
heightDiff = 50 - height;
// create and append tooltip
let tooltip = d3.select(element)
.append("g")
.style("filter", "url(#drop-shadow)")
.attr("class", "tooltip-container")
.attr("id", function(d, i) {
return elementName + "-tooltip-container"
})
.style("opacity", "0")
// create and append drop shadow for tooltip
var defs = tooltip.append("defs");
var filter = defs.append("filter")
.attr("id", "drop-shadow")
.attr("height", "130%");
var feOffset = filter.append("feOffset")
.attr("in", "SourceAlpha")
.attr("dy", 1)
.attr("result", "offset");
var feGaussianBlur = filter.append("feGaussianBlur")
.attr("id", "blur-ie")
.attr("stdDeviation", 2)
.attr("result", "blur");
var feFlood = filter.append("feFlood")
.attr("result", "flood")
.attr("flood-color", "#000000")
.attr("flood-opacity", 0.35);
var feComposite = filter.append("feComposite")
.attr("result", "composite")
.attr("operator", "in")
.attr("in2", "blur")
var feBlend = filter.append("feBlend")
.attr("result", "blend")
.attr("in", "SourceGraphic")
var feMerge = filter.append("feMerge");
feMerge.append("feMergeNode")
.attr("in", "SourceAlpha");
feMerge.append("feMergeNode")
.attr("in", "SourceGraphic");
feMerge.append("feMergeNode")
.attr("in2", "blur");
d3.select("#blur-ie").attr("stdDeviation", 2)
var range = [0, width];
var x = d3.scaleLinear()
.range(range)
.domain([0, width]);
var y = d3.scaleLinear()
.range(range)
.domain([0, width]);
var poly;
calloutDirection === "top-right" ?
(poly = [{ "x": 0, "y": height },
{ "x": (width - 50), "y": height },
{ "x": (width - 35), "y": (height + 15) },
{ "x": (width - 35), "y": (height + 15) },
{ "x": (width - 20), "y": height },
{ "x": width, "y": height },
{ "x": width, "y": 0 },
{ "x": 0, "y": 0 }
], y.range([width, 0])) :
calloutDirection === "top-left" ?
(poly = [{ "x": 0, "y": height },
{ "x": 20, "y": height },
{ "x": 35, "y": (height + 15) },
{ "x": 35, "y": (height + 15) },
{ "x": 50, "y": height },
{ "x": width, "y": height },
{ "x": width, "y": 0 },
{ "x": 0, "y": 0 }
], y.range([width, 0])) :
calloutDirection === "bottom-left" ?
(poly = [{ "x": 0, "y": height },
{ "x": 20, "y": height },
{ "x": 35, "y": (height + 15) },
{ "x": 35, "y": (height + 15) },
{ "x": 50, "y": height },
{ "x": width, "y": height },
{ "x": width, "y": 0 },
{ "x": 0, "y": 0 }
]) :
calloutDirection === "bottom-right" ?
(poly = [{ "x": 0, "y": height },
{ "x": (width - 50), "y": height },
{ "x": (width - 35), "y": (height + 15) },
{ "x": (width - 35), "y": (height + 15) },
{ "x": (width - 20), "y": height },
{ "x": width, "y": height },
{ "x": width, "y": 0 },
{ "x": 0, "y": 0 }
]) :
null;
tooltip.selectAll("polygon")
.data([poly])
.enter()
.append("polygon")
.attr("points", function(d, i) {
return d.map(function(d) {
return [x(d.x), y(d.y)].join(",");
})
.join(" ");
})
.attr("stroke", "#ffffff")
.attr("stroke-linejoin", "round")
.attr("stroke-width", "2")
.attr("class", "tooltip-content")
.attr("id", function(d, i) {
return elementName + "-tooltip-content"
})
}
}
这是其中一张图表的代码:
function(element, data, categories) {
// set the linechart data and dimensions
let width = t.width - margin.left - margin.right,
height = t.height - margin.top - margin.bottom,
lcData = [Object.values(data)[0], Object.values(data)[1], categories],
max = (Math.ceil(Math.max(...lcData[0], ...lcData[1]) / 10) * 10),
min = Math.min(...lcData[0], ...lcData[1]);
// create arrays for x and y of both lcDatas for mouseover
var lcDataX = [],
lcDataOneY = [],
lcDataTwoY = [];
// set the y-scale
let yScale = d3.scaleLinear()
.domain([0, max])
.range([height - 70, 0])
.nice();
//set the x-scale
let xScale = d3.scaleBand()
.domain(lcData[2])
.rangeRound([0, width - 30]);
// create a line
let line = d3.line()
.x(function(d, i) {
return xScale(lcData[2][i]) + xScale.bandwidth() / 2;
})
.y(function(d) {
return yScale(d);
})
// create svg container
let lineChart = d3.select(element)
.append("svg")
.attr("class", "linechart")
.attr("viewBox", "0 0 440 285")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.style("display", "block")
.append("g")
.attr("class", "linechart-group")
.attr("transform", "translate(53.5, 20)")
// create and append the legend
let legendGroup = lineChart.append("g")
.attr("class", "lc-legend")
.attr("transform", "translate(-26, 247)")
// create and append the colored legend squares
legendGroup.append("rect")
.attr("class", "lc-data-one-legend")
.attr("height", 18)
.attr("width", 18)
.attr("x", (width / 2))
legendGroup.append("rect")
.attr("class", "lc-data-two-legend")
.attr("height", 18)
.attr("width", 18)
.attr("x", (width / 4))
// create and append the legend labels
legendGroup.append("text")
.attr("class", "lc-data-one-legend-label")
.attr("x", (width * .57))
.attr("y", ((height * .1) * .5))
.text(function(){
return Object.keys(data)[1]
})
legendGroup.append("text")
.attr("class", "lc-data-two-legend-label")
.attr("x", (width * .32))
.attr("y", ((height * .1) * .50))
.text(function() {
return Object.keys(data)[0]
})
// create the x gridlines
let xGridlines = d3.axisLeft()
.tickFormat("")
.ticks(3)
.tickSize(-(width - 58))
.scale(yScale.nice(3));
// create the y gridlines
let yGridlines = d3.axisBottom()
.tickFormat("")
.ticks(12)
.tickSize(-(height - 40))
.scale(xScale);
// create a group for the x- and y-Gridlines and remove the domain for it
lineChart.append("g")
.attr("class", "lc-x-gridlines")
.attr("transform", "translate(13, 10)")
.call(xGridlines)
.select(".domain").remove();
lineChart.append("g")
.attr("class", "lc-y-gridlines")
.attr("transform", "translate(0, 210)")
.call(yGridlines)
.select(".domain").remove();
// create a group for the x-axis and append it
lineChart.append("g")
.attr("class", "lc-xaxis")
.attr("transform", "translate(0, 205)")
.call(d3.axisBottom(xScale)
.tickSizeOuter(0)
)
.selectAll("text")
.attr("x", -24)
.attr("y", -4)
.attr("dy", "10")
.attr("transform", "rotate(-45)")
.attr("id", function(d, i) {
return "lc-xaxis-tick-" + i
})
.on("mouseover", function(d, i) {
linechartmouseover(this, element, i);
})
.on("mouseout", function(d, i) {
linechartMouseout(this, element, i);
});
// create a group for the y-axis and append it
lineChart.append("g")
.attr("class", "lc-yaxis")
.attr("transform", "translate(0, 10)")
.call(d3.axisLeft(yScale)
.ticks(3)
.tickFormat(function(d) {
return (d >= 1000 ? thousandFormat(d) : d);
})
)
// create a group for the line
let lineGroup = lineChart.append('g')
.attr("class", "line-group")
.attr("transform", "translate(-1, 10)")
// append the first line to the svg group
lineGroup.append("path")
.datum(lcData[0])
.attr("d", line)
.attr("class", "lc-data-one-line")
// append the second line to the svg group
lineGroup.append("path")
.datum(lcData[1])
.attr("d", line)
.attr("class", "lc-data-two-line")
// create a group for the points
let points = lineChart.append('g')
.attr("class", "points-group")
.attr("transform", "translate(-1, 7)")
// create and append both set of points
for (let i = 0; i < lcData.length - 1; i++) {
// assign value of parent i to n
let n = i;
// create points and append them for each dataset
points.selectAll("dot")
.data(lcData[i])
.enter()
.append("rect")
.attr("height", 6)
.attr("width", 6)
.attr("x", function(d, i) {
let x = xScale(lcData[2][i]) + xScale.bandwidth() / 2.5;
lcDataX.push(x);
return x;
})
.attr("y", function(d) {
n == 0 ? (lcDataOneY.push(yScale(d))) : (lcDataTwoY.push(yScale(d)));
return yScale(d)
})
.attr("class", function(d, i) {
return (n == 0 ? "lc-data-one-points" : "lc-data-two-points");
})
.attr("id", function(d, i) {
return (n == 0 ? ("lc-data-one-point-" + i) : ("lc-data-two-point-" + i));
})
.on("mouseover", function(d, i) {
linechartmouseover(this, element, i);
})
.on("mouseout", function(d, i) {
linechartMouseout(this, element, i);
});
}
// add mouseover to gridlines
d3.selectAll(element + " .lc-y-gridlines .tick line")
.on("mouseover", function(d, i) {
linechartmouseover(this, element, i);
})
.on("mouseout", function(d, i) {
linechartMouseout(this, element, i);
});
function linechartmouseover(element, id, i) {
// create Y coordinate array based on the points with the highest value
var lcDataY = lcDataOneY.map(function(n, i) {
return (n < lcDataTwoY[i] || lcDataTwoY[i] == undefined) ? n : lcDataTwoY[i];
})
var x,
y = lcDataY[i] - 49,
direction,
elem = id;
// set the x based on the dataset
(i === 0) ? x = lcDataX[0] - 2 : (i === 1) ? x = lcDataX[1] - 2 : x = lcDataX[i] - 2;
// define the direction of the tooltip's point based on positioning of x and y on the chart
(i < lcData[0].length / 2 && y > 0) ? direction = "bottom-left"
: (i < lcData[0].length / 2 && y <= 0) ? direction = "top-left"
: (i >= lcData[0].length / 2 && y > 0) ? direction = "bottom-right"
: direction = "top-right";
//remove previous tooltips
d3.selectAll("#linechart-tooltip-container").remove()
//create tooltip
createTooltip(elem + " .linechart", "linechart", 138, 64, direction, true);
// define tooltip variable and variables for x and y positioning of tooltip
let tooltip = d3.select("#linechart-tooltip-container"),
xCoord,
yCoord;
// define the adjustments needed to the tooltip position based on tooltip point direction
direction === "top-right" ?
(xCoord = x - 46.5, yCoord = y + 26) :
direction === "bottom-left" ?
(xCoord = x + 22, yCoord = y - 7) :
direction === "top-left" ?
(xCoord = x + 21.5, yCoord = y + 26) :
direction === "bottom-right" ?
(xCoord = x - 46.5, yCoord = y - 6) :
null;
// show and translate tooltip
tooltip.style("display", "inline")
.attr("transform", "translate(" + xCoord + "," + yCoord + ")")
.transition()
.ease(d3.easeSin)
.style("opacity", "1")
.duration(300)
// create and append labels for all datasets
for(let j = 0; j < lcData.length -1; j++) {
// append the label of the dataset to the tooltip
tooltip.append("text")
.attr("class", "tooltip-label")
.attr("id", "tooltip-label" + j)
.attr("x", 20)
.attr("y", function(d) {
return (j === 1 ? ((direction == "top-right" || direction == "top-left") ? 118 : 44) :
((direction == "top-right" || direction == "top-left") ? 102 : 28))
})
.text(function(){
return Object.keys(data)[j] + ":";
})
// get the bounding box of the label to dynamically position the value next to it
let tlabel = d3.select("#tooltip-label" + j);
let tlabelBox = tlabel.node().getBBox();
// append dataset value to the tooltip
tooltip.append("text")
.attr("class", "tooltip-value")
.attr("x", function(){
return (tlabelBox.width + tlabelBox.x + 4)
})
.attr("y", function(d) {
return (j === 1 ? ((direction == "top-right" || direction == "top-left") ? 118 : 44) :
((direction == "top-right" || direction == "top-left") ? 102 : 28))
})
.text(function() {
return (lcData[j][i] !== undefined) ? commaFormat(lcData[j][i]) : "N/A"
})
// select points specific to each dataset
let point = (j == 0 ? (id + " #lc-data-one-point-" + i) : (id + " #lc-data-two-point-" + i));
// highlight corresponding points
d3.select(point)
.attr("height", 8)
.attr("width", 8)
.attr("transform", function(d) {
return j == 0 ? "translate(-1, 0)" : "translate(-2, 0)"
})
}
// increase font size of xaxis on mouseover
d3.select(id + " #lc-xaxis-tick-" + i)
.attr("font-size", 14)
.attr("transform", "rotate(-45) translate(-4,0)")
}
function linechartMouseout(element, id, i) {
for (let j = 0; j < lcData.length; j++) {
// select points specific to each dataset
let point = (j == 0 ? (id + " #lc-data-one-point-" + i) : (id + " #lc-data-two-point-" + i));
// remove highlight from corresponding points
d3.select(point)
.attr("height", 6)
.attr("width", 6)
.attr("transform", "translate(0,0)")
}
// reduce font size of xaxis
d3.select(id + " #lc-xaxis-tick-" + i)
.attr("font-size", 12)
.attr("transform", "rotate(-45) translate(0,0)")
// hide tooltip on mouseout
d3.selectAll("#linechart-tooltip-container")
.transition()
.ease(d3.easeSin)
.style("opacity", "0")
.duration(300)
}
}
实际结果: 我必须对支持图表的特定宽度进行硬编码。
所需结果: 我可以在文本元素的任意一侧动态生成20像素的工具提示宽度/高度。在这种情况下,有两个文本元素,但并不是每个图形都有两个。