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

LineChart: add custom tooltip just above a point #488

Open
veej opened this issue Jan 31, 2017 · 67 comments
Open

LineChart: add custom tooltip just above a point #488

veej opened this issue Jan 31, 2017 · 67 comments
Labels
enhancement Enhancement to a current API

Comments

@veej
Copy link

veej commented Jan 31, 2017

Hi everyone,
in my project I'm trying to achieve something like in the image below, where the tooltip is rendered just above the point it refers to.

image

Looking at the props the custom tooltip receives, coordinates follow the mouse cursor. Is there a way to have coordinates fixed to the point in the graph?

@xile611 xile611 added the enhancement Enhancement to a current API label Feb 8, 2017
@Odrin
Copy link

Odrin commented Mar 3, 2017

Have same problem with bar chart when trying place tooltip on the top of bar. It's possible to change tooltip's position on bar mouse enter, but not on cursor render.

@vkarpusha
Copy link

The same problem, how set tooltip to top of bar.

@Odrin
Copy link

Odrin commented Mar 9, 2017

My workaround with area chart:

import React from 'react';
import {
  AreaChart,
  XAxis,
  YAxis,
  Tooltip,
  ResponsiveContainer,
  Area,
} from 'recharts';

export default class UIAreaChart extends React.PureComponent {
  static propTypes = {
    data: React.PropTypes.array.isRequired
  };

  constructor(props) {
    super(props);

    this.area = null;
    this.tooltip = null;
    this.point = null;

    this.onChartMouseMove = this.onChartMouseMove.bind(this);
    this.onChartMouseLeave = this.onChartMouseLeave.bind(this);
  }

  onChartMouseMove(chart) {
    if (chart.isTooltipActive) {
      let point = this.area.props.points[chart.activeTooltipIndex];

      if (point != this.point) {
        this.point = point;
        this.updateTooltip();
      }
    }
  }

  onChartMouseLeave() {
    this.point = null;
    this.updateTooltip();
  }

  updateTooltip() {
    if (this.point) {
      let x = Math.round(this.point.x);
      let y = Math.round(this.point.y);

      this.tooltip.style.opacity = '1';
      this.tooltip.style.transform = `translate(${x}px, ${y}px)`;
      this.tooltip.childNodes[0].innerHTML = this.point.payload['value'];
    }
    else {
      this.tooltip.style.opacity = '0';
    }
  }

  render() {
    return (
      <div className="ui-chart">
        <ResponsiveContainer width="100%" height="100%">
          <AreaChart data={this.props.data}
                     onMouseMove={this.onChartMouseMove}
                     onMouseLeave={this.onChartMouseLeave}>
            <XAxis dataKey="date" />
            <YAxis />
            <Tooltip />
            <Area ref={ref => this.area = ref} type="monotone" dataKey="value"/>
          </AreaChart>
        </ResponsiveContainer>
        <div className="ui-chart-tooltip" ref={ref => this.tooltip = ref}>
          <div className="ui-chart-tooltip-content"></div>
        </div>
      </div>
    );
  }
}

@matheusml
Copy link

matheusml commented May 10, 2017

Since this is a very common case, shouldn't this be added to the library?
Something like:

<Tooltip position="top" />

@xile611

@benjazehr
Copy link

Thx a lot @Odrin. Could you maybe show what your css (ui-chart, ui-chart-tooltip) looks like?

@Odrin
Copy link

Odrin commented Jun 5, 2017

@angelozehr nothing special:

.ui-chart {
  position: relative;
  ...
}
.ui-chart-tooltip {
  pointer-events: none;
  opacity: 0;
  position: absolute;
  top: 0;
  left: 0;
  z-index: 10;
  ... and lots of beautifying styles
}

@shailajashah31
Copy link

Any updates on position="top"? I am still confused regarding how to show tooltip on top of a data point?
Could you please help me?

@maxefi
Copy link

maxefi commented Feb 6, 2018

@Odrin daysaver, thx m8.

adopted the solution for LineChart aswell.

@christinajensen
Copy link

+1 for positioning tooltips at the top of bars in bar chart 😊

@Lelith
Copy link

Lelith commented Apr 3, 2018

Any updates on this topic?

@nathmack
Copy link

+1

1 similar comment
@valaz
Copy link

valaz commented Apr 26, 2018

+1

@alonecuzzo
Copy link

+1 need update

@revanthbomma
Copy link

+1

2 similar comments
@solnikita
Copy link

+1

@nucab
Copy link

nucab commented Jun 26, 2018

+1

@jesperlandmer
Copy link

+1
@Odrin getting undefined from this:
let point = this.area.props.points[chart.activeTooltipIndex];

Seems like there is no points-key in area-props. Am I missing something?

@Odrin
Copy link

Odrin commented Jun 28, 2018

@jesperlandmer Perhaps something has changed in api over the past year, I didn't test it on new versions.

@UpsideDownRide
Copy link

+1

3 similar comments
@akrstic01
Copy link

+1

@erosenberg
Copy link

+1

@natasha-tullos
Copy link

+1

@erosenberg
Copy link

Why is this still not a feature?

@rajatkanthaliya12
Copy link

Using below logic you can achieve individual tool-tip for each dot.

Demo Link: Line chart with custom Tooltip

  1. Hide default Tooltip

    <Tooltip cursor={false} wrapperStyle={{ display: "none" }} />

  2. Add mouse event function on Line (when dot is active)

     <Line
           activeDot={{
             onMouseOver: this.showToolTip,
             onMouseLeave: this.hideToolTip
           }}
          ....
         />
    
  3. custom tooltip div

       <div className="ui-chart-tooltip" ref={ref => (this.tooltip = ref)} >
           <div className="ui-chart-tooltip-content" />
       </div>
    
  4. showToolTip and hideTooltip Function

          showToolTip = (e) => {
           let x = Math.round(e.cx);
           let y = Math.round(e.cy);
           this.tooltip.style.opacity = "1";
           this.tooltip.childNode[0].innerHTML = e.payload["value"];
    
           };
    
           hideTooltip = e => {
           this.tooltip.style.opacity = "0";
           };
    

@senelithperera
Copy link

@rajatkanthaliya12 Will this work for other charts also like barcharts ?

@mabzzz
Copy link

mabzzz commented Feb 18, 2020

Thanks @Odrin ! Your workaround works very well 👍

@xile611
Copy link
Member

xile611 commented Mar 17, 2020

I think there are may workaround now.

@xile611 xile611 closed this as completed Mar 17, 2020
@BossBele
Copy link

THIS IS THE BEST WAY TO DO IT:

import React, { Component } from 'react';
import {
  ResponsiveContainer, LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend,
} from 'recharts';
import { colors } from '../pages/_app';
import moment from 'moment';

export default class Linechart extends Component {
  constructor(props){
		super(props)
  }
  state = {
    index: 0
  }

  render() {
    const data = this.props.data;
    const CustomTooltip = ({ active, payload, label }) => {
      let index = this.state.index;
      if (active) {
        return (
          <div className="custom-tooltip">
            <p className="label">{`${label} : ${payload[index].value}`}</p>
            <p className="intro">{`${label} : ${payload[index].name}`}</p>
            <p className="desc">Anything you want can be displayed here.</p>
          </div>
        );
      }
      return null;
    };

    return (
        <div style={{ width:'100%', height:'100%' }}>
        <ResponsiveContainer>
            <LineChart
              width={500}
              height={300}
              data={data}
              margin={{
                top: 5, right: 30, left: 0, bottom: 5,
              }}
            >
                <CartesianGrid strokeDasharray="3 3" />
                <XAxis tick={{fontSize: 10}} dataKey="date" tickFormatter={val => moment(val).format("Do MMM")} />
                <YAxis tick={{fontSize: 10}} />
                <Tooltip content={<CustomTooltip />} />
                <Legend wrapperStyle={{fontSize:12, width:'100%'}} />
                <Line activeDot={{
                  onMouseOver: ()=>this.setState({index:0})
                }} type="monotone" dataKey="lease" name="Lease to Own" stroke='#00ACC0' activeDot={{ r: 8 }} />
                <Line activeDot={{
                  onMouseOver: ()=>this.setState({index:1})
                }} type="monotone" dataKey="mortgage" name="Mortgages" stroke='#F9A825' />
                <Line activeDot={{
                  onMouseOver: ()=>this.setState({index:2})
                }} type="monotone" dataKey="construction" name="Construction Finance" stroke='#5BBC9C' />
                <Line activeDot={{
                  onMouseOver: ()=>this.setState({index:3})
                }} type="monotone" dataKey="investment" name="Investments" stroke='#E64A19' />
            </LineChart>
        </ResponsiveContainer>
        </div>
    );
  }
}

The trick here is to render a custom tooltip, and display data pertaining to the hovered line on the tooltip.
For the tooltip:
<Tooltip content={<CustomTooltip />} />
For each line add:

activeDot={{
onMouseOver: ()=>this.setState({index:1})
}}

To make it like:

<Line activeDot={{
onMouseOver: ()=>this.setState({index:1})
}} type="monotone" dataKey="mortgage" name="Mortgages" stroke='#F9A825' />

Add your custom tooltip:

const CustomTooltip = ({ active, payload, label }) => {
      let index = this.state.index;
      if (active) {
        return (
          <div className="custom-tooltip">
            <p className="label">{`${label} : ${payload[index].value}`}</p>
            <p className="intro">{`${label} : ${payload[index].name}`}</p>
            <p className="desc">Anything you want can be displayed here.</p>
          </div>
        );
      }
      return null;
};

The custom tooltip will display data of the nth line: where n starts from 0 (index number of line).
For my case, I have 4 lines so the index numbers of the lines start from 0 to 3.
The trick is to update the index of the custom tooltip to match the index of the line being hovered, And for this, I have used the 'index' state.

@Miloupost1988
Copy link

+1

1 similar comment
@qlaire
Copy link

qlaire commented Oct 14, 2021

+1

@klintan
Copy link

klintan commented Oct 21, 2021

Here is a solution I came up with for getting the tooltip on top of each bar whenever you are in the cursor area:

export default function BarChartCard(props) {
  const [activeBarIndex, setActiveBarIndex] = useState();
  const [toolTipYPosition, setToolTipYPosition] = useState({});


  useEffect(() => {
      const barCharts = document.querySelectorAll(".recharts-bar-rectangle");
      if (!barCharts || !activeBarIndex) return;
      // Init tooltip values
      const barChart = barCharts[activeBarIndex];
      setToolTipYPosition(barChart.getBoundingClientRect().height);
  }, [activeBarIndex]);

  return (
    <BaseCard>
      <Grid container spacing={2}>
        <Grid item xs={12}>
          <Typography variant="h2">{props.title}</Typography>
        </Grid>
      </Grid>
      <ResponsiveContainer width={"100%"} height={300}>
        <BarChart
          barGap={0}
          data={props.data}
          margin={{
            top: 5,
            right: 30,
            left: 20,
            bottom: 5,
          }}
          onMouseMove={(e) => {
            setActiveBarIndex(e.activeTooltipIndex);
          }}
        >
          <Tooltip
            content={<CustomTooltip />}
            cursor={false}
            position={{ y: 170 - toolTipYPosition }}
            offset={-60}
            allowEscapeViewBox={{ x: true, y: true }}
          />
          <XAxis
            style={{
              fontSize: "1.125rem",
              fontFamily: "Cairo",
              fontWeight: 600,
            }}
            axisLine={false}
            tickLine={false}
            dataKey="date"
            tickFormatter={ReformatDateShortDayName}
          />
          <YAxis
            allowDecimals={false}
            style={{
              fontSize: "1.125rem",
              fontFamily: "Cairo",
              fontWeight: 600,
            }}
            dataKey="value"
            axisLine={false}
            tickLine={false}
          />
          <Legend
            style={{
              fontSize: "1.125rem",
              fontFamily: "Cairo",
              fontWeight: 600,
            }}
            iconType="circle"
            iconSize={6}
            align="right"
            verticalAlign="top"
            height={36}
            formatter={(value) => (
              <span
                style={{
                  color: "#CCC",
                  fontSize: "1.125rem",
                  fontWeight: 600,
                  fontFamily: "Cairo",
                }}
              >
                {value}
              </span>
            )}
          />
          <Bar
            barSize={10}
            dataKey="value"
            name={props.legendText}
            fill="#EFC92B"
          />
        </BarChart>
      </ResponsiveContainer>
    </BaseCard>
  );
}

More info here https://stackoverflow.com/questions/55310451/how-to-position-tooltip-on-the-top-of-a-barchart-in-rechart/69581293#69581293

@alcaroff
Copy link

+1

9 similar comments
@MuhammadObeidat
Copy link

+1

@olefrank
Copy link

+1

@vinceprofeta
Copy link

+1

@Tarasikee
Copy link

+1

@totoledao
Copy link

+1

@bmamatahir
Copy link

+1

@dongransu
Copy link

+1

@antongosu
Copy link

+1

@antongosu
Copy link

+1

@pdykmann96
Copy link

Here is a solution for a CustomTooltip over the active Dot.
I solved it by saving the position of the dots when the 'animation of the line End' (onAnimationEnd) and updating the current active Position by onMouseMove Event of LineChart.

import React, { useCallback, useEffect, useState } from 'react';
import { Grid } from '@mui/material';
import {
  Line,
  LineChart,
  XAxis,
  YAxis,
  CartesianGrid,
  Tooltip,
  ResponsiveContainer,
} from 'recharts';
import { ChartTooltip } from './style';

const CustomChart = ({ defaultPosition }: {defaultPosition: number}) => {
//Dots in the LineChart
  const [dots, setDots] = useState<{ x: number, y: number }[]>([]);
// current Active Position of active Dot
  const [activeDotPosition, setActiveDotPosition] = useState<{x: number | undefined, y: number | undefined}>({ x: undefined, y: undefined });
// current active Value which should be displayed in the Tooltip
  const [activeValue, setActiveValue] = useState<number | undefined>(undefined);

  const data = [
    {
        "name": "0",
        "value": 60000
    },
    {
        "name": "1",
        "value": 56400
    },
    {
        "name": "2",
        "value": 53016
    },
    {
        "name": "3",
        "value": 49835
    },
    {
        "name": "4",
        "value": 46845
    },
    {
        "name": "5",
        "value": 44034
    },
    {
        "name": "6",
        "value": 41392
    },
    {
        "name": "7",
        "value": 38909
    },
]

  // set TooltipPosition on Default at the defaultPosition Dot
  const setDefaultTooltipPosition = () => {
// Find Dots Position by custom dot id given in 'customDot'
    const xCoordinate = (document.querySelector(`[id=dot_${defaultPosition}]`) as HTMLElement)?.getAttribute('cx') || undefined;
    const yCoordinate = (document.querySelector(`[id=dot_${defaultPosition}]`) as HTMLElement)?.getAttribute('cy') || undefined;
    if (!!xCoordinate && !!yCoordinate) {
      setActiveValue(data.find((dataObject) => dataObject.name === defaultPosition)?.value);
      setActiveDotPosition({ x: parseInt(xCoordinate, 10), y: parseInt(yCoordinate, 10) });
    }
  };

//Find every Dot by custom id and save dot positions in dots
  const setDotsData = () => {
    const dotsData: {x: number, y: number}[] = [];
	// index <= 7 bc name in data.name is max. 7
    for (let index = 0; index <= 7; index++) {
      const xCoordinate = (document.querySelector(`[id=dot_${index}]`) as HTMLElement)?.getAttribute('cx') || undefined;
      const yCoordinate = (document.querySelector(`[id=dot_${index}]`) as HTMLElement)?.getAttribute('cy') || undefined;
      if (!!xCoordinate && !!yCoordinate) {
        dotsData.push({ x: parseInt(xCoordinate, 10), y: parseInt(yCoordinate, 10) });
      }
    }
    if (dotsData.length) {
      setDots(dotsData);
    }
  };

// Custom Dot with given custom id to use in querySelector (style can be added as well)
  const customDot = ({ cx, cy, payload }: {cx: number, cy: number, payload: {value: number, name: string}}) => { return ( <circle
	cx={cx}
	cy={cy}
	r={0.5}
	stroke="black"
	strokeWidth={3}
	fill={'black'}
	data-testid={payload.name}
	id={`dot_${payload.name}`}
   />)}
 ;

// Set Dots Data after animation (rendering also done) and place default Tooltip
  const handleAnimationEnd = () => {
    setDotsData();
    setDefaultTooltipPosition();
  };

// Set Current active Position and current Active Value of Tooltip on every Mouse Move (onMouseMove)
  const setActiveData = (props: any) => {
    if (dots.length && props.activeTooltipIndex !== undefined) {
      const { x, y } = dots[props.activeTooltipIndex];
      if (activeDotPosition.x !== x || activeDotPosition.y !== y) {
        setActiveDotPosition({ x, y });
      }
      if (activeValue !== data[props.activeTooltipIndex].value) {
        setActiveValue(data[props.activeTooltipIndex].value);
      }
    }
  };

// CustomTooltip in active and default position
  const CustomTooltip = useCallback(({ active, payload }: {active: boolean, payload: {value: number, name: string}[]}) => {
    if (active && payload?.length) {
      const value = payload[0].value + ' €';
      return <ChartTooltip>{value}</ChartTooltip>;
    }
    if (!active && activeValue) {
      const value = activeValue + ' €';
      return <ChartTooltip>{value}</ChartTooltip>;
    }
    return null;
  }, [activeValue]);

 
  useEffect(() => {
    setActiveDotPosition({ x: undefined, y: undefined });
    setDefaultTooltipPosition();
  }, []);


  return (
    <Grid container>
        <ResponsiveContainer
          width="100%"
          height={240}
        >
          <LineChart
            data={data}
            margin={{ top: 5, right: 30, left: 20, bottom: 5 }}
            onMouseMove={(e) => setActiveData(e)}
          >
            <CartesianGrid horizontal={false} stroke={"#5A5A5A"} />
            <XAxis
              axisLine={false}
              tickLine={false}
              tickMargin={10}
              dataKey="name"
              tick={{ fontSize: '12px', color: '#333333' }}
            />
            <YAxis
              tickLine={false}
              axisLine={false}
              unit="€"
            />
            <Tooltip
              content={<CustomTooltip />}
              isAnimationActive={false}
              position={activeDotPosition}
              allowEscapeViewBox={{ x: true, y: true }}
              wrapperStyle={{
                visibility: activeDotPosition.x !== undefined && activeDotPosition.y !== undefined ? 'visible' : 'hidden',
              }}
            />
            <Line
              type="monotone"
              dataKey="value"
              stroke={'#FF0000'}
              strokeWidth={2}
              animationDuration={500}
              onAnimationEnd={handleAnimationEnd}
			  dot={customDot}
            />
          </LineChart>
        </ResponsiveContainer>
    </Grid>
  );
};

export default CustomChart;

And here the CustomTooltipElement with styled components:

export const ChartTooltip = styled.div`
  --triangle-left: 18px;
  --triangle-top: 25px;
  border: 2px solid #FF0000;
  padding: 6px 10px;
  border-radius: 20px;
  font-size: 14px;
  font-weight: 400;
  position: relative;
  left: -26px;
  bottom: 50px;
  background: white;
// SpeechBubble Tooltip
  ::after {
    content: '';
    display: block;
    height: 10px;
    width: 10px;
    background-color: white;
    border-top: 2px solid #FF0000;
    border-left: 2px solid #FF0000;
    position: absolute;
    top: var(--triangle-top);
    left: var(--triangle-left);
    transform: rotate(-135deg);
    z-index: 3;
  }
   ::before {
    content: '';
    display: block;
    height: 4px;
    width: 14px;
    background-color: white;
    position: absolute;
    top: calc(var(--triangle-top) + 1px);
    left: calc(var(--triangle-left) - 1px);
    z-index: 2;
  } 
`;

In this solution there is a fixed Tooltip at a fixed position (defaultPosition) when not hovering over the Chart as default. If you're not using it just remove 'defaultPosition, setDefaultTooltipPosition, useEffect'

@mhenke96
Copy link

mhenke96 commented Sep 5, 2022

Such functionality should be core. Maybe we should reopen this issue?

@Krishrawat
Copy link

Why this issue has been closed when there is still no core functionality available for this?

@FastestMolasses
Copy link

Ridiculous that this isn't a core feature. There are only a handful of "solutions" that don't even work. This entire system needs to be refactored. Adding small features shouldn't be this messy.

@ElMagxd
Copy link

ElMagxd commented Sep 27, 2022

Still a thing

@cuongtq17
Copy link

             <LineChart onMouseMove={e => {
             
              const tooltip = document.querySelector(
                '.recharts-tooltip-wrapper'
              ) as HTMLElement;
                
              if (!tooltip) return;
              
              const c: Point = {
                x: e.activeCoordinate?.x || 0,
                y: e.activeCoordinate?.y || 0
              };
              
              const tooltipWidth = tooltip.clientWidth;

              setActivePoint({
                x: c.x - tooltipWidth / 2,
                y: c.y - 200
              });
            }}></LineChart>

then on the tooltip
<Tooltip position={activePoint} cursor={false} content={ <CustomToolTip /> }
As simple as that

@sankaSanjeeva
Copy link

In my case below method worked fine for me.

define customActiveDot

const renderActiveDot = (props: any) => {
  const { cx, cy, value } = props;

  return (
    <svg
      x={cx - 27}
      y={cy - 55}
      width="54"
      height="68"
      viewBox="0 0 54 68"
      xmlns="http://www.w3.org/2000/svg"
    >
      <rect
        x="13"
        y="40"
        width="28"
        height="28"
        rx="14"
        fill="#58CD91"
        fillOpacity="0.3"
      />
      <path
        d="M25.7929 44.2071L26.5 44.9142L27.2071 44.2071L34.4142 37H40C47.1797 37 53 31.1797 53 24V14C53 6.8203 47.1797 1 40 1H14C6.8203 1 1 6.8203 1 14V24C1 31.1797 6.8203 37 14 37H18.5858L25.7929 44.2071Z"
        fill="#F2F2F2"
        stroke="white"
        strokeWidth="2"
      />
      <text textAnchor="middle" dy={26} dx={27} fontSize={20}>
        {value}
      </text>
      <rect
        x="19.9297"
        y="47"
        width="14"
        height="14"
        rx="7"
        fill="#58CD91"
        stroke="white"
        strokeWidth="2"
      />
    </svg>
  );
};

Apply like this in <LineChart>...<LineChart />

<Tooltip content={() => null} />

<Line
  dataKey="score"
  stroke="#58CD91"
  strokeWidth={2}
  activeDot={renderActiveDot}
  dot={false}
/>

Screenshot 2023-01-04 at 11 05 42

@wopian
Copy link

wopian commented Mar 9, 2023

While the method mentioned above using the activeDot component does work, it is bound to the chart's container, so points at the edges of the chart will be cut off.

@ckifer ckifer reopened this Mar 9, 2023
@ashhher
Copy link

ashhher commented May 5, 2023

@wopian that is because the overflow property of the outside svg is set to "hidden", you can fix this by setting it to "visible" in your css code.

I change the code from @sankaSanjeeva a little bit, in case someone still wants to use the tooltip provided by recharts

We can create a state to synchronize the position of activeDot to the tooltip. But for this way, we need to specify which dataKey we want to follow, otherwise the state will keep updating. here is the link of an example and my code:
https://codesandbox.io/s/recharts-tooltip-follow-datapoint-hsz163?file=/src/App.tsx

import "./styles.css";

import { useState } from "react";

import {
  AreaChart,
  XAxis,
  YAxis,
  CartesianGrid,
  Tooltip,
  Area,
  ResponsiveContainer
} from "recharts";

function App() {
  const data = [
    {
      name: "Page A",
      uv: 4000,
      pv: 2400,
      amt: 2400
    },
    {
      name: "Page B",
      uv: 3000,
      pv: 1398,
      amt: 2210
    },
    {
      name: "Page C",
      uv: 2000,
      pv: 9800,
      amt: 2290
    },
    {
      name: "Page D",
      uv: 2780,
      pv: 3908,
      amt: 2000
    },
    {
      name: "Page E",
      uv: 1890,
      pv: 4800,
      amt: 2181
    },
    {
      name: "Page F",
      uv: 2390,
      pv: 3800,
      amt: 2500
    },
    {
      name: "Page G",
      uv: 3490,
      pv: 4300,
      amt: 2100
    }
  ];

  type RADType = {
    cx: number;
    cy: number;
    dataKey: string;
  };

  const [tooltipPos, setTooltipPos] = useState<{ x: number; y: number }>();

  const renderActiveDot = ({ cx, cy, dataKey }: RADType) => {
    if (dataKey === "pv") {
      if (tooltipPos?.x !== cx && tooltipPos?.y !== cy)
        setTooltipPos({ x: cx, y: cy });
    }
    return <></>;
  };

  return (
    <div className="pageContainer">
      <div className="chartContainer">
        <ResponsiveContainer>
          <AreaChart
            width={730}
            height={250}
            data={data}
            margin={{ top: 10, right: 30, left: 0, bottom: 0 }}
            className="chartWrapper"
          >
            <defs>
              <linearGradient id="colorUv" x1="0" y1="0" x2="0" y2="1">
                <stop offset="5%" stopColor="#8884d8" stopOpacity={0.8} />
                <stop offset="95%" stopColor="#8884d8" stopOpacity={0} />
              </linearGradient>
              <linearGradient id="colorPv" x1="0" y1="0" x2="0" y2="1">
                <stop offset="5%" stopColor="#82ca9d" stopOpacity={0.8} />
                <stop offset="95%" stopColor="#82ca9d" stopOpacity={0} />
              </linearGradient>
            </defs>
            <XAxis dataKey="name" />
            <YAxis />
            <CartesianGrid strokeDasharray="3 3" />
            <Tooltip position={tooltipPos} />
            <Area
              type="monotone"
              dataKey="uv"
              stroke="#8884d8"
              fillOpacity={1}
              fill="url(#colorUv)"
              activeDot={renderActiveDot}
            />
            <Area
              type="monotone"
              dataKey="pv"
              stroke="#82ca9d"
              fillOpacity={1}
              fill="url(#colorPv)"
              activeDot={renderActiveDot}
            />
          </AreaChart>
        </ResponsiveContainer>
      </div>
    </div>
  );
}

export default App;

But still, please we really need this functionality 😢

@nikolasrieble
Copy link
Contributor

To communicate transparently, adding new features is not high on our TODO list. We would rather first finish migrating the documentation to storybook, fixing bugs, and refactoring the code to functional components.

All that work will enable us to finally focus on adding features again, but for now we need to make the code base easier to work with.

That being said, if you would like to give it a shoot, I would love to review your PR :) We are always looking for contributors.

@ettenger
Copy link

if (tooltipPos?.x !== cx && tooltipPos?.y !== cy)

@ashhher thank you for the great solution! One small tweak to your code. The quoted code should be if (tooltipPos?.x !== cx || tooltipPos?.y !== cy) so the tooltip moves when subsequent data points have the same value.

@alettieri
Copy link

alettieri commented Mar 7, 2024

It took me some time to come up with a solution for one of our charts. There's probably a much better approach we can take but without looking through the recharts codebase I am not clear on what that would look like. Would be curious what others have been able to do.

Screenshot 2024-03-07 at 9 40 48 AM

Here is a gist for the code: https://gist.github.com/alettieri/e5bfa8982e0cd2c38ebc6b3bf94a0462

In short, I used the Line components ref to merge some points and read the x coordinate off those. Then created a custom Tooltip content component and used a transform to center the tooltip over the point.

Here is the code that really matters:

const getLinePointItems = (
    projectedPoints?: Array<LinePointItem>,
    actualPoints?: Array<LinePointItem>
) => {
    if (projectedPoints && actualPoints) {
        // Merge the actual and projected points together
        // We want to keep the indexes aligned so we can use the index to find the correct point
        return actualPoints.map((point, index) => {
            if (point.x && point.y) {
                return point;
            }
            return projectedPoints[index];
        });
    }
    return [];
};
const PlanItemChart = ({
    planItem,
    projectedRule,
    group,
    planId,
}: {
    planItem: PlanItem;
    projectedRule: IPlanItemProjectedRule;
    group: PlanItemGroup;
    planId: Plan['id'];
}) => {
    const [toolTipIndex, updateToolTipIndex] = React.useState(0);
    const theme = useTheme();

    const actualRef = React.useRef<null | (Line & SVGPathElement)>(null);
    const projectedRef = React.useRef<null | (Line & SVGPathElement)>(null);

   //... cut out some code for brevity

    const handleMouseMove = React.useCallback<OnMouseMoveHandler>(
        (coords) => {
            if (
                coords.isTooltipActive &&
                coords.activeTooltipIndex !== undefined
            ) {
                updateToolTipIndex(coords.activeTooltipIndex);
            }
        },
        [updateToolTipIndex]
    );

    const toolTipPosition = React.useMemo<
        TooltipProps<number, string>['position']
    >(() => {
        const linePointItems = getLinePointItems(
            projectedRef.current?.props?.points,
            actualRef.current?.props?.points
        );

        if (linePointItems[toolTipIndex]) {
            // Grab the current tooltip position based off the point index list
            const point = linePointItems[toolTipIndex];
            return { x: point.x, y: point.y };
        }
        return undefined;
    }, [toolTipIndex]);

// ... left out some code for brevity
    return (
                <LineChart
                    data={createPlanVersionPreview.data?.[0]}
                    margin={{ right: 8, left: 8 }}
                    onMouseMove={handleMouseMove}
                >
                    <Tooltip
                        content={<PlanItemChartTooltip />}
                        position={toolTipPosition}
                    />
                    <Line
                        type="monotone"
                        dataKey="p"
                        stroke={theme.palette.grey[500]}
                        activeDot={{ r: 4 }}
                        dot={{ r: 2 }}
                        ref={projectedRef}
                    />
                    <Line
                        type="monotone"
                        dataKey="a"
                        stroke={theme.palette.primary.main}
                        activeDot={{ r: 4 }}
                        dot={{ r: 2 }}
                        ref={actualRef}
                    />
                    <Legend
                        verticalAlign="bottom"
                        wrapperStyle={{
                            bottom: 0,
                            fontSize: '10px',
                        }}
                        content={<PlanItemChartLegend />}
                    />
                </LineChart>
   
    );
};

The refs are read here:

                    <Line
                        type="monotone"
                        dataKey="p"
                        stroke={theme.palette.grey[500]}
                        activeDot={{ r: 4 }}
                        dot={{ r: 2 }}
                        ref={projectedRef}
                    />
                    <Line
                        type="monotone"
                        dataKey="a"
                        stroke={theme.palette.primary.main}
                        activeDot={{ r: 4 }}
                        dot={{ r: 2 }}
                        ref={actualRef}
                    />
);

Adding the custom Tooltip content component so it's easier to reference.

import React from 'react';

import { Typography, Box, SxProps, Theme, alpha } from '@mui/material';
import { TooltipProps } from 'recharts';

import { IPlanVersionPreviewResponse } from '../../shared/types/plan';
import { convertNumberToCurrency } from '../../utils/currency-utils';

const styles: SxProps<Theme> = {
    '--_height': '48px',
    '--_width': '68px',
    '--_offsetX': '-50%',
    '--_offsetY': 'calc(-1 * (var(--_height) + 18px))',
    '--_tooltip-bg': (theme) => alpha(theme.palette.grey[700], 0.9),
    display: 'flex',
    flexDirection: 'column',
    gap: 1,
    textAlign: 'center',
    minWidth: 'var(--_width)',
    height: 'var(--_height)',
    p: '10px',
    backgroundColor: 'var(--_tooltip-bg)',
    color: 'common.white',
    fontSize: 'tooltip.fontSize',
    fontWeight: 'tooltip.fontWeight',
    transform: `translate(var(--_offsetX), var(--_offsetY))`,
    borderRadius: 1,
    position: 'relative',
    overflow: 'clip-content',
    '& .PlanItemChartTooltip-arrow': {
        position: 'absolute',
        bottom: '-7px',
        left: '50%',
        width: 0,
        height: 0,
        borderLeft: '8px solid transparent',
        borderRight: '8px solid transparent',
        borderTop: '8px solid var(--_tooltip-bg)',
        ml: '-8px',
    },
};

export const PlanItemChartTooltip = (props: TooltipProps<number, string>) => {
    const toolTipContent = React.useMemo<null | {
        value: string;
        date: string;
    }>(() => {
        if (props.payload.length > 0) {
            const payload = props.payload[0];
            const dataKey = payload?.dataKey as 'p' | 'a';
            const dataPoint =
                payload.payload as IPlanVersionPreviewResponse[0][0];
            const value = Reflect.get(dataPoint, dataKey);
            const date = dataPoint.d;
            return { value: convertNumberToCurrency(value), date };
        }
        return null;
    }, [props.payload]);

    if (toolTipContent !== null) {
        return (
            <Box className="PlanItemChartTooltip-root" sx={styles}>
                <Typography variant="inherit">{toolTipContent.date}</Typography>
                <Typography variant="inherit">
                    {toolTipContent.value}
                </Typography>
                <Box className="PlanItemChartTooltip-arrow" />
            </Box>
        );
    }

    return null;
};

@NarendraKuruva
Copy link

NarendraKuruva commented Sep 3, 2024

I've tried it like this. And it worked in my use case

Here is the codesandbox link

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement Enhancement to a current API
Projects
None yet
Development

No branches or pull requests