D3.js - настроить ширину всплывающей подсказки на основе текста внутри - PullRequest
0 голосов
/ 31 декабря 2018

Живой код: 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 с любой стороны текстовых элементов.В этом случае есть два текстовых элемента, но не каждый граф будет иметь два.

...