D3.js-根据内部文字调整工具提示宽度

时间:2018-12-31 14:16:17

标签: javascript d3.js svg position tooltip

实时代码: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像素的工具提示宽度/高度。在这种情况下,有两个文本元素,但并不是每个图形都有两个。

0 个答案:

没有答案