Bounding HTML Tooltip inside listener

Viewed 46

I am visualizing data using d3.js. Within the visualization, I have built a HTML tooltip. I have overlaid a svg rect to listen to the desired mouseevents before it can generate the tooltip. It has been good so far.

However, I desire the tooltip to adjust its position in a way so that it always stays inside the listening rect. But I can't seem to find a way to do that.

The code is below

const data = [{ x: 1, y: .03 }, { x: 2, y: .04 }, { x: 3, y: .06 }, { x: 4, y: .09 }, { x: 5, y: .13 }, { x: 6, y: .18 }, { x: 7, y: .25 }, { x: 8, y: .33 }, { x: 9, y: .45 }, { x: 10, y: .59 }, { x: 11, y: .76 }, { x: 12, y: .97 }, { x: 13, y: 1.22 }, { x: 14, y: 1.52 }, { x: 15, y: 1.85 }, { x: 16, y: 2.24 }, { x: 17, y: 2.66 }, { x: 18, y: 3.13 }, { x: 19, y: 3.63 }, { x: 20, y: 4.16 }, { x: 21, y: 4.71 }, { x: 22, y: 5.27 }, { x: 23, y: 5.83 }, { x: 24, y: 6.38 }, { x: 25, y: 6.91 }, { x: 26, y: 7.4 }, { x: 27, y: 7.85 }, { x: 28, y: 8.25 }, { x: 29, y: 8.59 }, { x: 30, y: 8.86 }, { x: 31, y: 9.06 }, { x: 32, y: 9.18 }, { x: 33, y: 9.24 }, { x: 34, y: 9.22 }, { x: 35, y: 9.15 }, { x: 36, y: 9.01 }, { x: 37, y: 8.83 }, { x: 38, y: 8.6 }, { x: 39, y: 8.34 }, { x: 40, y: 8.05 }, { x: 41, y: 7.74 }, { x: 42, y: 7.41 }, { x: 43, y: 7.08 }, { x: 44, y: 6.76 }, { x: 45, y: 6.43 }, { x: 46, y: 6.11 }, { x: 47, y: 5.81 }, { x: 48, y: 5.51 }, { x: 49, y: 5.24 }, { x: 50, y: 4.98 }, { x: 51, y: 4.74 }, { x: 52, y: 4.52 }, { x: 53, y: 4.32 }, { x: 54, y: 4.15 }, { x: 55, y: 3.99 }, { x: 56, y: 3.84 }, { x: 57, y: 3.71 }, { x: 58, y: 3.59 }, { x: 59, y: 3.49 }, { x: 60, y: 3.39 }, { x: 61, y: 3.31 }, { x: 62, y: 3.23 }, { x: 63, y: 3.15 }, { x: 64, y: 3.09 }, { x: 65, y: 3.02 }, { x: 66, y: 2.97 }, { x: 67, y: 2.91 }, { x: 68, y: 2.86 }, { x: 69, y: 2.81 }, { x: 70, y: 2.77 }, { x: 71, y: 2.73 }, { x: 72, y: 2.68 }, { x: 73, y: 2.64 }, { x: 74, y: 2.6 }, { x: 75, y: 2.56 }, { x: 76, y: 2.52 }, { x: 77, y: 2.47 }, { x: 78, y: 2.43 }, { x: 79, y: 2.39 }, { x: 80, y: 2.34 }, { x: 81, y: 2.29 }, { x: 82, y: 2.24 }, { x: 83, y: 2.19 }, { x: 84, y: 2.13 }, { x: 85, y: 2.08 }, { x: 86, y: 2.02 }, { x: 87, y: 1.96 }, { x: 88, y: 1.89 }, { x: 89, y: 1.83 }, { x: 90, y: 1.76 }, { x: 91, y: 1.69 }, { x: 92, y: 1.62 }, { x: 93, y: 1.54 }, { x: 94, y: 1.47 }, { x: 95, y: 1.39 }, { x: 96, y: 1.32 }, { x: 97, y: 1.24 }, { x: 98, y: 1.17 }, { x: 99, y: 1.09 }, { x: 100, y: 1.02 }];

//------------------------0.DATA WRANGLING------------------------//

const xVal = [...new Set(data.map(d => d.x))];
const median = d3.median(xVal);

//------------------------1.CREATE SVG------------------------//

//define dimension
const width = 1280;
const height = 600;

const main = d3.select('div')
    .style('position', 'relative');

//HTML DIV for tooltip
const div =
    main.append('div')
    .attr('class', 'tooltip')
    .style('opacity', '0')
    .style('position', 'absolute')
    .style('display', 'flex')
    .style('background', 'yellow');



//namespace
const svgns = 'http://www.w3.org/2000/svg'

main
    .append('svg')
    .attr('xmlns', svgns)
    .attr('viewBox', `0 0 ${width} ${height}`)
    .attr('id', 'svg')

const svg = d3.select('svg')

svg.append('rect')
    .attr('class', 'vBoxRect')
    .attr('width', `${width}`)
    .attr('height', `${height}`)
    .attr('fill', 'none')
    .attr('stroke', 'red');

//------------------------2. CREATE BOUND------------------------//    
const padding = {
    top: 70,
    bottom: 50,
    left: 70,
    right: 50
};

const boundHeight = height - padding.top - padding.bottom;
const boundWidth = width - padding.right - padding.left;

//create BOUND rect -- to be deleted later
svg.append('rect')
    .attr('class', 'boundRect')
    .attr('x', `${padding.left}`)
    .attr('y', `${padding.top}`)
    .attr('width', `${boundWidth}`)
    .attr('height', `${boundHeight}`)
    .attr('fill', 'none')
    .attr('stroke', 'green')

//create bound element
const bound = svg.append('g')
    .attr('class', 'bound')
    .style('transform', `translate(${padding.left}px,${padding.top}px)`);


//------------------------3. CREATE SCALE------------------------//
const scaleX = d3.scaleLinear()
    .range([0, boundWidth])
    .domain(d3.extent(data, d => d.x));

const scaleY = d3.scaleLinear()
    .range([boundHeight, 0])
    .domain(d3.extent(data, d => d.y));

//------------------------4. CREATE AXIS------------------------//       

//create Y Axis    
bound.append('g').attr('class', 'yAxis')
    .call(d3.axisLeft(scaleY))
    .attr('class', 'yAxis');

//create X Axis Bottom
bound.append('g')
    .attr('class', 'xAxis')
    .append('g')
    .attr('class', 'xAxisBottom')
    .call(d3.axisBottom(scaleX))
    .style('transform', `translateY(${boundHeight}px)`);

//------------------------5. CREATE PATH------------------------//     

// Add the line
bound
    .append("path")
    .datum(data)
    .attr("fill", "none")
    .attr("stroke", "steelblue")
    .attr("stroke-width", 1.5)
    .attr("d", d3.line()
        .x(d => scaleX(d.x))
        .y(d => scaleY(d.y)));

//------------------------6. CREATE INTERACTION------------------------//         
// Create the text that travels along the curve of chart
const focusText = bound
    .append('g')
    .attr('class', 'travText')
    .append('text')
    .attr("opacity", 0);

// listening Rect
const listener =
    bound
    .append('rect')
    .style("fill", "none")
    .attr('class', 'listeningRect')
    .style("pointer-events", "all")
    .attr('width', boundWidth)
    .attr('height', boundHeight);


//------------------------B. PROGRAM INTERACTION--------------//
listener
    .on('mouseover', function() {
        focusText.attr("opacity", 1);
        div.style('opacity', '1');
    })
    .on('mouseout', function() {
        focusText.attr("opacity", 0);
        div.style('opacity', '0');
    })
    .on('mousemove', function(event) {
        const mouseCoord = d3.pointer(event);
        const mouseCoordX = mouseCoord[0];
        const invertX = scaleX.invert(mouseCoordX);
        const rnd = Math.round(invertX);
        const src = data.filter(a => a.x === rnd)[0];

        focusText
            .html("SVG Element-x:" + src.x + "  -  " + "y:" + src.y)
            .attr("x", () => ((src.x < median) ?
                scaleX(src.x) :
                scaleX(src.x)
            ))
            .attr("y", scaleY(src.y))
            .attr("text-anchor",
                () => (src.x < median) ?
                'start' : (src.x > median) ? 'end' : 'auto');

        div.html("HTML Element-x:" + src.x + "  -  " + "y:" + src.y)
            .style('top', `${event.pageY}px`)
            .style(`left`, `${event.pageX}px`)

    });
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
  </head>
  <script type="text/javascript" src="https://d3js.org/d3.v7.min.js"></script>
  <body>
    <div id="viz"></div>
  </body>
</html>

To elaborate, if I also create a SVG tooltip, I can do the following which can cheaply ensure that the svg text auto-adjusts its position and width wise it always stays inside the listening rect. How can I achieve the similar for the HTML element?

.attr("text-anchor", () => (src.x < median) ? 'start' : (src.x > median) ? 'end' : 'auto');

0 Answers
Related