Живой код: https://blockbuilder.org/ashleighc207/ef14357436ffe981f1cd5a841b8a8558
У меня есть библиотека диаграмм, полная различных диаграмм D3.js (линии, гистограммы, карты и т. Д.), Которые все используют одну функцию для создания своих подсказок.Всплывающая подсказка создается как многоугольник SVG (специально для поддержки IE, который, да, я должен).В настоящее время я передаю параметры ширины и высоты в функцию всплывающей подсказки, которая жестко запрограммирована на основе графика.
Это не супер-динамично и становится проблемой, когда у вас длинные слова / большие цифры.Я хотел бы основывать ширину и высоту на основе (при условии лучшего метода здесь) ограничительной рамки, или BBox, текстовых элементов.В Интернете я наткнулся на пару решений, которые предлагают создать всплывающую подсказку, добавить текст и перенастроить ширину / высоту всплывающей подсказки после.
Я обнаружил две проблемы с указанным подходом - во-первых, проблемы с Safariдругие мои вычисления BBox, когда к тексту добавляются пустые текстовые элементы до того, как получит указанный 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)
}
}
Фактические результаты: Мне нужно жестко кодировать определенную ширину, которая поддерживает диаграмму.
Желаемые результаты: я могу динамически генерировать ширину / высоту всплывающей подсказки с 20px с любой стороны текстовых элементов.В этом случае есть два текстовых элемента, но не каждый граф будет иметь два.