Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

dynamically determine best tooltip anchor #580

Closed
humanchimp opened this issue Jun 5, 2019 · 35 comments
Closed

dynamically determine best tooltip anchor #580

humanchimp opened this issue Jun 5, 2019 · 35 comments

Comments

@humanchimp
Copy link
Contributor

humanchimp commented Jun 5, 2019

Version of nivo tested: 0.58.0

Is your feature request related to a problem? Please describe.
Our concern/feature request is primarily related to the line chart, but this affects other chart types too.

After upgrading to a recent version of nivo, the tooltips stopped being positioned relatively such that they don't overflow the bounding box. In a previous version we were using (0.31.0, specifically), the tooltip would adjust by anchoring itself on the opposite side of the cursor when the pointer reached the midpoint. Now it seems the scheme has changed and an anchor attribute must be provided.

I don't think this option will let us dynamically position the tooltip to avoid colliding with the boundary.

Describe the solution you'd like
It would be better for us if the tooltip would dynamically anchor itself like it used to. Alternatively, we'd like to be able to use a hook to determine the best anchor dynamically

Describe alternatives you've considered
I considered sending a PR to modify the positioning logic, but I don't feel comfortable with this approach.

https://github.com/plouc/nivo/blob/master/packages/tooltip/src/hooks.js#L29-L41

const bounds = container.current.getBoundingClientRect()
let x = event.clientX - bounds.left
let y = event.clientY - bounds.top

if (anchor === 'left' || anchor === 'right') {
  if (x < bounds.width / 2) anchor = 'right'
  else anchor = 'left'
}

Basically, I cannot imagine something like this being mergeable, although it does work well for my application.

I would like to solve this problem by potentially allowing an "auto" option for anchor, or something like that.

Additional context
I sincerely hope this is a feature request, and not simply a demonstration of ignorance on my part. I tried to figure this out myself before resorting to this request

@chirgjn
Copy link

chirgjn commented Jul 26, 2019

Hey if no one is working on this, should I submit a PR
I need this fix for a project at work 😁

@chirgjn
Copy link

chirgjn commented Jul 27, 2019

@plouc is the change by @humanchimp enough? I was thing to check the bounds of the tooltip with the container to determine if it fits and change the anchor if that's not the case.

Also what are your thoughts on letting the user opt into this behaviour via a flag in props? Because sometimes I'm ok when my tooltips get outside the container.

@shihlinlu
Copy link

Agreed with @chirgjn about the flag prop option instead of making it dynamic in all cases.

@humanchimp
Copy link
Contributor Author

i don't think nivo is deprecated? As the author of this issue, I was doubtful that my approach, which was just copypasta from elsewhere in the codebase, would be high quality enough to merge. I have been using a fork of just the @nivo/tooltip package which applies my patch, and that works for my needs, and means that actually having my changes merged upstream is not a priority. If not having #631 merged in is causing you difficulty, I recommend doing likewise for now.

@kkkrist
Copy link

kkkrist commented Oct 25, 2019

I'm using your patch and it works great for my use cases. Btw, I'm applying it via patch-package, so I don't need to fork it or change my npm deps.

Also I just want to say I appreciate your humility very much.

@yocontra
Copy link

Has anyone managed to get this to work on both the X and Y axis? The code above works great for X but Y is a little trickier, since you need to compute the height of the tooltip to do it AFAIK.

Repository owner deleted a comment from vintg Dec 25, 2019
Repository owner deleted a comment from vintg Dec 25, 2019
Repository owner deleted a comment from kkkrist Dec 25, 2019
Repository owner deleted a comment from vintg Dec 25, 2019
@serj-prog
Copy link

The issue still exists. Is anybody going to work on it?

@jeffshek
Copy link

jeffshek commented Aug 28, 2020

A workaround I used here was to use a css position: absolute on the tooltip. (Thx for your work on Nivo, this package is amazzzzzing!)

<ResponsiveLine tooltip={TooltipFormat}/>

const TooltipFormat = ({ point }, ...rest) => {
    // example, app.betterself.io/demo 
    return <StyledDivWithAbsolutePosition/>
}

image

@Luke1298
Copy link

Luke1298 commented Nov 2, 2020

It would also be cool if we could choose which way we wanted our tooltips to anchor; it seems like the library is choosing right for tooltip "slices" and top for everything else.

@stale
Copy link

stale bot commented Jan 31, 2021

This issue has been automatically marked as stale. If this issue is still affecting you, please leave any comment (for example, "bump"), and we'll keep it open. We are sorry that we haven't been able to prioritize it yet. If you have any new additional information, please include it with your comment!

@stale stale bot added the stale label Jan 31, 2021
@8annoy
Copy link

8annoy commented Feb 6, 2021

bump

@stale stale bot removed the stale label Feb 6, 2021
@ozcanonur
Copy link

ozcanonur commented Feb 9, 2021

Bump. I solved mine with position absolute and dynamically setting left/right depending on the hover position. You can get the hover position from the node prop that is given by you for the custom tooltip. Thanks, @jeffshek.

@celestemartins
Copy link

Bump. I solved mine with position absolute and dynamically setting left/right depending on the hover position. You can get the hover position from the node prop that is given by you for the custom tooltip. Thanks, @jeffshek.

Hey @ozcanonur can you share how did you do that?
I'm facing the same issue here. Thanks!

@LeninSG21
Copy link

@celestemartins I did the following:

  1. Send a prop to my Tooltip component indicating the number of elements in the x-axis

  2. Check whether the given point is in the first or second half of the chart like so:

    const isFirstHalf = point.index < numElementsX / 2;

  3. Apply the appropriate css style, depending on what you want to achieve.

ex.

import React from 'react';
import { Point } from '@nivo/line';

const CustomTooltip = ({ point, numElementsX } : { point: Point, numElementsX: number }) => {
  const isFirstHalf = point.index < numElementsX / 2;
  return (
     <div 
       style = {{
         position: 'absolute',
         left: isFirstHalf ? 30 : 0,
         right: isFirstHalf ? 0 : 30,
       }}
      >
         // your tooltip
      </div>
    );
};

Note: I actually used clsx to select the appropriate css class. But it is the same concept

@admc
Copy link

admc commented Mar 8, 2021

update: the left / right styles and absolute positioning seem to be a reasonable temporary fix.

Love this project. Currently working on a ResponsiveSwarmPlot and struggling with this issue and coming from recharts where the tooltip finds a way to always stay in the viewport, i'd like to replicate that behavior. My company would be happy to fund someones work to get a thorough / high quality patch in to the project if that helps! Thanks again.

@bunge12
Copy link

bunge12 commented Apr 9, 2021

@celestemartins I did the following:

  1. Send a prop to my Tooltip component indicating the number of elements in the x-axis
  2. Check whether the given point is in the first or second half of the chart like so:
    const isFirstHalf = point.index < numElementsX / 2;
  3. Apply the appropriate css style, depending on what you want to achieve.

ex.

import React from 'react';
import { Point } from '@nivo/line';

const CustomTooltip = ({ point, numElementsX } : { point: Point, numElementsX: number }) => {
  const isFirstHalf = point.index < numElementsX / 2;
  return (
     <div 
       style = {{
         position: 'absolute',
         left: isFirstHalf ? 30 : 0,
         right: isFirstHalf ? 0 : 30,
       }}
      >
         // your tooltip
      </div>
    );
};

Note: I actually used clsx to select the appropriate css class. But it is the same concept

Thanks for your help!

@admc
Copy link

admc commented Apr 14, 2021

This approach was really helpful for me, I wound up using the onMouseMove event and ResizeObserver to keep track of the width and height of the container, and the mouse position which made it pretty easy to figure out in which quadrant of the graph the tooltip would be rendered and adjust the top/left offsets accordingly!

@hadasmaimon
Copy link

you can workaround with breaking word:

word-wrap: break-word; /* All browsers since IE 5.5+ */
overflow-wrap: break-word; /* Renamed property in CSS3 draft spec */
max-width: 10rem;

@stale
Copy link

stale bot commented Sep 27, 2021

This issue has been automatically marked as stale. If this issue is still affecting you, please leave any comment (for example, "bump"), and we'll keep it open. We are sorry that we haven't been able to prioritize it yet. If you have any new additional information, please include it with your comment!

@stale stale bot added the stale label Sep 27, 2021
@ncknuna
Copy link

ncknuna commented Sep 27, 2021

bump :)

@stale stale bot removed the stale label Sep 27, 2021
@wiznotwiz
Copy link

Bump

3 similar comments
@rohanj-0606
Copy link

Bump

@rohanj-0606
Copy link

Bump

@tpulido
Copy link

tpulido commented Nov 19, 2021

Bump

@tpulido
Copy link

tpulido commented Dec 1, 2021

@celestemartins I did the following:

  1. Send a prop to my Tooltip component indicating the number of elements in the x-axis
  2. Check whether the given point is in the first or second half of the chart like so:
    const isFirstHalf = point.index < numElementsX / 2;
  3. Apply the appropriate css style, depending on what you want to achieve.

ex.

import React from 'react';
import { Point } from '@nivo/line';

const CustomTooltip = ({ point, numElementsX } : { point: Point, numElementsX: number }) => {
  const isFirstHalf = point.index < numElementsX / 2;
  return (
     <div 
       style = {{
         position: 'absolute',
         left: isFirstHalf ? 30 : 0,
         right: isFirstHalf ? 0 : 30,
       }}
      >
         // your tooltip
      </div>
    );
};

Note: I actually used clsx to select the appropriate css class. But it is the same concept

Thanks! This also helped me for a bar chart Tooltip, just using transform instead of left/right different.

const Tooltip = (props) => {
  const { bar } = props;
  const isFirstHalf = bar.index <= 6;

  return (
    <div
      className="chart-tooltip"
      style={{
        position: "absolute",
        transform: isFirstHalf ? "translate(0,0)" : "translate(-260px,0)",
      }}
    >
     // ...content
    </div>
  );
};

@amack-butter
Copy link

First, thank you @plouc for the amazing Nivo library, and for others for posting their wrappers. Is there any plan to make these changes an official part of the library?

@doiali
Copy link

doiali commented Feb 6, 2022

bump

@Danamorah
Copy link

Thanks for the examples i did the following using <ResponsiveLine/>:
the <div> also have an position:absolute

ex.

import React from 'react';

const basicTooltip = (props : any) => {
  const {point} = props
  const isFirstHalf = point.x < 941 / 2;
  return (
     <div
        style={isFirstHalf ? { left: 0 } : { right: 0 }}

         //tooltip content
      </div>
    );
};

<ResponsiveLine
     tooltip={basicTooltip}
/>

@pramodkandel
Copy link

bump

@stale
Copy link

stale bot commented Jun 18, 2022

This issue has been automatically marked as stale. If this issue is still affecting you, please leave any comment (for example, "bump"), and we'll keep it open. We are sorry that we haven't been able to prioritize it yet. If you have any new additional information, please include it with your comment!

@stale stale bot added the stale label Jun 18, 2022
@ncknuna
Copy link

ncknuna commented Jun 21, 2022

bump

@stale
Copy link

stale bot commented Jul 6, 2022

Closing this issue after a prolonged period of inactivity. If this issue is still present in the latest release, please create a new issue with up-to-date information. Thank you!

@studiosciences
Copy link

studiosciences commented Dec 16, 2022

I ended up creating a new custom layer and used our UI component library for the tooltip. This was fairly straight forward, worked better, and was more consistent with our overall UX.

@damianr13
Copy link

I know the issue is closed but someone might reach this issue by googling the problem, same as I did.

The solutions above are either specific for bar charts, or they use hardcoded values like the 941 width in @Danamorah 's example.

Instead of determining wether one is on the left or right part of the chart, I propose determining wether the tooltip fits to the left and if so, show it there. I also address the some issue on the Y axis.

My code:

export interface NonOverflowTooltipWrapperProps {
  point: { x: number; y: number };
  innerComponent: React.ReactNode;
}

const NonOverflowTooltipWrapper = (props: NonOverflowTooltipWrapperProps) => {
  const [tooltipSize, setTooltipSize] = React.useState<{
    width: number;
    height: number;
  }>({ width: 0, height: 0 });

  // dynamically get the size of the tooltip
  React.useEffect(() => {
    const tooltip = document.querySelector(".nivo_tooltip");
    if (tooltip) {
      const { width, height } = tooltip.getBoundingClientRect();
      setTooltipSize({ width, height });
    }
  }, [setTooltipSize]);

  // only show it to the right of the pointer when we are close to the left edge
  const translateX = React.useMemo(
    () =>
      props.point.x < (tooltipSize.width * 1.3) / 2 ? 0 : -tooltipSize.width,
    [tooltipSize, props.point.x]
  );

  // only show it below the pointer when we are close to the top edge
  const translateY = React.useMemo(
    () =>
      props.point.y < (tooltipSize.height * 1.3) / 2 ? 0 : -tooltipSize.height,
    [tooltipSize, props.point.y]
  );

  return (
    <div
      className={"nivo_tooltip"}
      style={{
        position: "absolute",
        transform: `translate(${translateX}px, ${translateY}px)`,
        background: "#ffffff",
        padding: "12px 16px",
        width: "fit-content",
      }}
    >
      <div style={{ position: "relative" }}>{props.innerComponent}</div>
    </div>
  );
};

Using this component we don't need to know (or hardcode) the actual size of the chart.

@WilliamABradley
Copy link
Contributor

WilliamABradley commented Mar 3, 2024

Based on @damianr13's solution, I came up with this:

import { useState, useEffect, useMemo, useRef } from "react";

export interface NonOverflowTooltipProps {
  point: { x: number; y: number };
  container: React.RefObject<HTMLDivElement>;
  children: React.ReactNode;
}

export function NonOverflowTooltip(props: NonOverflowTooltipProps) {
  const ref = useRef<HTMLDivElement>(null);
  const [containerSize, setContainerSize] = useState<{
    width: number;
    height: number;
  }>({ width: 0, height: 0 });
  const [tooltipSize, setTooltipSize] = useState<{
    width: number;
    height: number;
  }>({ width: 0, height: 0 });

  // dynamically get the size of the container
  useEffect(() => {
    const container = props.container.current;
    if (container) {
      const { width, height } = container.getBoundingClientRect();
      setContainerSize({ width, height });
    }
  }, [setContainerSize, props.container]);

  // dynamically get the size of the tooltip
  useEffect(() => {
    const tooltip = ref.current;
    if (tooltip) {
      const { width, height } = tooltip.getBoundingClientRect();
      setTooltipSize({ width, height });
    }
  }, [setTooltipSize]);

  const offsetHorizontal = useMemo(() => {
    // only show it to the right of the pointer when we are close to the left edge
    if (props.point.x < tooltipSize.width) {
      return tooltipSize.width / 3;
    }

    // only show it to the left of the pointer when we are close to the right edge
    const rightEdge = containerSize.width - props.point.x;
    if (rightEdge < tooltipSize.width) {
      return -(tooltipSize.width / 3);
    }

    return 0;
  }, [tooltipSize.width, props.point.x, containerSize.width]);

  const offsetVertical = useMemo(() => {
    // only show it above the pointer when we are close to the bottom edge
    if (props.point.y > containerSize.height - tooltipSize.height) {
      return -tooltipSize.height;
    }

    const bottomEdge = containerSize.height - props.point.y;
    if (bottomEdge < tooltipSize.height) {
      return -tooltipSize.height;
    }

    return 0;
  }, [tooltipSize.height, props.point.y, containerSize.height]);

  return (
    <div
      ref={ref}
      style={{
        position: "relative",
        left: offsetHorizontal,
        right: 0,
        top: offsetVertical,
        bottom: 0,
      }}
    >
      {props.children}
    </div>
  );
}

The benefit here is the layout and style is calculated by the children props, rather than the weird layout sizing I got using that solution + the tooltip will be joined with the crosshair.

Switched the query selector with react refs, so multiple graphs won't cause tooltip layout issues, however, a parent container ref needs to be passed in to get the surrounding bounds (For right side/bottom side positioning).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests