【问题标题】:How to position tooltip on the top of a barchart in rechart?如何将工具提示定位在 rechart 条形图的顶部?
【发布时间】:2019-03-23 03:50:54
【问题描述】:

问题: 我创建了一个带有自定义工具提示的条形图。现在我需要将工具提示放置在条形顶部而不是图表区域内。就像这张照片一样。

这就是现在的样子。

我在这里提供我如何组织我的代码。

import React, { Component } from "react";
import {
  BarChart,
  Tooltip,
  Bar,
  Legend,
  ResponsiveContainer,
  Cell
} from "recharts";

import { Card, CardTitle, CardBody } from "reactstrap";

import "./SessionDuration.css";

const colors = ["#26a0a7", "#79d69f", "#f9ec86", "#ec983d"];

const data = [
  {
    name: "Page A",
    pv: 2400,
    amt: 2400
  },
  {
    name: "Page B",
    pv: 1398,
    amt: 2210
  },
  {
    name: "Page C",
    pv: 9800,
    amt: 2290
  },
  {
    name: "Page D",
    pv: 3908,
    amt: 2000
  }
];

const getTitleOfTooltip = (label) =>{
  if (label ===  0) {
    return "<=5 min";
  }
  if (label === 1) {
    return "5-30 min";
  }
  if (label === 2) {
    return "30-60 min";
  }
  if (label === 3) {
    return ">60";
  }
}
const getIntroOfPage = label => {
  if (label ===  0) {
    return "Page A is about men's clothing";
  }
  if (label === 1) {
    return "Page B is about women's dress";
  }
  if (label === 2) {
    return "Page C is about women's bag";
  }
  if (label === 3) {
    return "Page D is about household goods";
  }
};

class SessionDuration extends Component {
  render() {
    return (
      <Card className="session-duration-card">
        <CardTitle className="session-duration-card-header">
          Session Duration
        </CardTitle>
        <CardBody>
          <ResponsiveContainer width="100%" height="100%" aspect={4.0 / 5.5}>
            <BarChart
              data={data}
              margin={{
                top: 3,
                right: 5,
                left: 5,
                bottom: 13
              }}
              barGap={10}
            >
              <Tooltip
                coordinate={{ x: 0, y: 150 }}
                content={<CustomTooltip />}
              />

              <Bar dataKey="pv" fill="#8884d8">
                {data.map((entry, index) => (
                  <Cell key={`cell-${index + 1}`} fill={colors[index]} />
                ))}
              </Bar>
            </BarChart>
          </ResponsiveContainer>
        </CardBody>
      </Card>
    );
  }
}

export default SessionDuration;

const CustomTooltip = ({ active, payload, label }) => {
  if (active) {
    return (
      <div className="session-duration-tooltip">
        <p className="session-duration-tooltip-label">{getTitleOfTooltip(label)}</p>
        <p className="session-duration-tooltip-intro">
          {getIntroOfPage(label)}
        </p>
        <p className="session-duration-tooltip-desc">
          Anything you want can be displayed here.
        </p>
      </div>
    );
  }

  return null;
};

我在这里提供我的 CSS 文件。

@media only screen and (min-width: 1024px) {
  .session-duration-card {
    margin-top: 14.5%;
    margin-left: -44%;
    width: 190% !important;
    border-radius: none !important;
    height: 86%

  }

  .session-duration-card-header {
    font-weight: 700;
    text-align: left;
    padding-left: 6%
  }

  .session-duration-tooltip {
    width: 210%;
  }

  .session-duration-tooltip-label {
    font-weight: 700;
    font-size: 11px;
  }
}

@media only screen and (min-width: 308px) and (max-width: 1024px) {
  .session-duration-card {
    margin-top: 5%;
    margin-left: 3.2%;
    width: 94.5%;
    border-radius: none !important;
  }

  .session-duration-card-header {
    font-weight: 700;
    text-align: center;
  }
}

@media only screen and (min-width: 1666px) {
  .session-duration-card {
    margin-top: 11%;
    margin-left: -13%;
    width: 190% !important;
    height: 97%;
    border-radius: none !important;
  }

  .session-duration-card-header {
    font-weight: 700;
    text-align: center;
  }
}

我尝试了很多方法来解决我的问题,但我无法完成它可以通过修改我的代码来帮助我完成这项工作。非常感谢。

【问题讨论】:

    标签: css tooltip bar-chart recharts


    【解决方案1】:

    要解决这个问题,需要获取每个bar和tooltip的高度和宽度。

    1. 需要两个函数onMouseMoveonMouseOut来获取高度 Bar 组件的宽度和宽度。
    2. 使用useState 获取并保持状态的高度和宽度,还为状态添加额外的布尔值,以在鼠标位于Bar之外时进行处理。 (因此,为了实现功能,我们添加了具有两个参数 databoolean 的对象。
    3. Tooltip 组件中,将 position 对象设置为静态值 XY 并使用状态值设置高度和宽度。
    4. 使用useEffect,查找.recharts-tooltip-wrapper类,定义DOM元素的高度和宽度。
    5. .recharts-tooltip-wrapper 设置样式。 (.recharts-tooltip-wrapper 中的 Rechart.js 有自己生成的样式,我们需要保留.recharts-tooltip-wrapper 的一些静态样式并添加我们的)。

    SessionDuration.js

    import React, { useState, useEffect } from 'react';
    import { BarChart, Tooltip, Bar, ResponsiveContainer, Cell } from 'recharts';
    
    import { Card, CardTitle, CardBody } from 'reactstrap';
    
    import './SessionDuration.css';
    
    const colors = ['#26a0a7', '#79d69f', '#f9ec86', '#ec983d'];
    
    const data = [
      {
        name: 'Page A',
        pv: 2400,
        amt: 2400,
      },
      {
        name: 'Page B',
        pv: 1398,
        amt: 2210,
      },
      {
        name: 'Page C',
        pv: 9800,
        amt: 2290,
      },
      {
        name: 'Page D',
        pv: 3908,
        amt: 2000,
      },
    ];
    
    const getTitleOfTooltip = label => {
      if (label === 0) {
        return '<=5 min';
      }
      if (label === 1) {
        return '5-30 min';
      }
      if (label === 2) {
        return '30-60 min';
      }
      if (label === 3) {
        return '>60';
      }
    };
    const getIntroOfPage = label => {
      if (label === 0) {
        return "Page A is about men's clothing";
      }
      if (label === 1) {
        return "Page B is about women's dress";
      }
      if (label === 2) {
        return "Page C is about women's bag";
      }
      if (label === 3) {
        return 'Page D is about household goods';
      }
    };
    
    export const SessionDuration = () => {
      const [position, setPosition] = useState(null);
    
      useEffect(() => {
        const tooltip = document.querySelector('.recharts-tooltip-wrapper');
        if (!tooltip) return;
    
        // Init tooltip values
        const tooltipHeight = tooltip.getBoundingClientRect().height;
        const tooltipWidth = tooltip.getBoundingClientRect().width;
        const spaceForLittleTriangle = 10;
    
        // Rewrite tooltip styles
        tooltip.style = `
          transform: translate(${position?.data.x}px, ${position?.data.y}px);
          pointer-events: none;  position: absolute;
          top: -${tooltipHeight + spaceForLittleTriangle}px;
          left: -${tooltipWidth / 2 - position?.data.width / 2}px;
          opacity: ${position?.show ? '1' : 0};
          transition: all 400ms ease 0s;
        `;
      }, [position]);
    
      return (
        <>
          <Card className="session-duration-card">
            <CardTitle className="session-duration-card-header">
              Session Duration
            </CardTitle>
            <CardBody>
              <ResponsiveContainer width="100%" height="100%" aspect={4.0 / 5.5}>
                <BarChart
                  data={data}
                  margin={{
                    top: 100, // for tooltip visibility
                    right: 5,
                    left: 5,
                    bottom: 13,
                  }}
                  barGap={10}
                >
                  <Tooltip
                    cursor={false} // hide hover effect 'grey rectangle'
                    position={{
                      // Static position
                      x: position?.data.x ?? 0,
                      y: position?.data.y ?? 0,
                    }}
                    content={<CustomTooltip />}
                  />
    
                  <Bar
                    dataKey="pv"
                    fill="#8884d8"
                    // Handlers
                    onMouseMove={data => setPosition({ data: data, show: true })}
                    onMouseOut={data => setPosition({ data: data, show: false })}
                  >
                    {data.map((entry, index) => (
                      <Cell key={`cell-${index + 1}`} fill={colors[index]} />
                    ))}
                  </Bar>
                </BarChart>
              </ResponsiveContainer>
            </CardBody>
          </Card>
        </>
      );
    };
    
    export default SessionDuration;
    
    const CustomTooltip = ({ active, payload, label }) => {
      if (active) {
        return (
          <div className="session-duration-tooltip">
            <p className="session-duration-tooltip-label">
              {getTitleOfTooltip(label)}
            </p>
            <p className="session-duration-tooltip-intro">
              {getIntroOfPage(label)}
            </p>
            <p className="session-duration-tooltip-desc">
              Anything you want can be displayed here.
            </p>
          </div>
        );
      }
    
      return null;
    };

    SessionDuration.css

    :root {
      --bg-color: hsl(270 3% 29% / 0.9);
    }
    
    .session-duration-tooltip {
      max-width: 250px;
      position: relative;
      padding: 0.5em;
      border-radius: 5px;
      background-color: var(--bg-color);
      color: aliceblue;
    }
    
    .session-duration-tooltip::after {
      content: '';
      height: 0;
      width: 0;
      position: absolute;
      bottom: -10px;
      left: 50%;
      transform: translateX(-50%);
      border-style: solid;
      border-width: 10px 10px 0 10px;
      border-color: var(--bg-color) transparent transparent transparent;
    }

    【讨论】:

      【解决方案2】:

      很多人在这里https://github.com/recharts/recharts/issues/222https://github.com/recharts/recharts/issues/488 要求此功能。我想出了

      第三个可行的解决方案(在@ЖнецЪ 回答的帮助下)

      不幸的是,这个答案的问题是你仍然必须悬停“栏”本身,而我的很窄,所以我希望它显示出来,无论你是悬停在栏还是“光标区域”,这个解决方案允许后者。

      基本上:

      • 从完整的条形图中获取活动的工具提示索引(光标区域的变化而不是特定的条形)

      • useEffect 挂钩使用条形类 ".recharts-bar-rectangle" 抓取所有条形,并使用 toolTipIndex 抓取当前活动的条形,获取高度,并将其设置为工具提示 Y 位置。

      • 和以前一样,工具提示由内部 x 位置控制,但 y 位置是高度 +/- 一些偏移量。

      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>
        );
      }
      
      

      一个 hacky 解决方案

      基于其他一些实现和答案:

      const [barGraphData, setBarGraphData] = useState({})
      
      return (
      <BarChart barGap={0} data={props.data} margin={{
                  top: 5,
                  right: 30,
                  left: 20,
                  bottom: 5
                  }}>
          <Tooltip content={<CustomTooltip />}
          cursor={{ fill: 'transparent' }}
          position={{ y: barGraphData.height + 40}}
          offset={-60}
          />
          <XAxis />
          <YAxis />
          <Legend />
          <Bar 
             barSize={10} 
             dataKey="value" 
             fill="#EFC92B" 
             onMouseOver={(data)=> {
                  setBarGraphData(data)
                  }}
             onMouseLeave={(data) => {
                  setBarGraphData(data)
                  }}
                  />
      </BarChart>
      )
      

      所以基本上:

      • 有一个静态 y 值,在这种情况下,我正在计算条形的高度 + 一些偏移量(至少我认为它是条形的高度)。但这个也可以是静态的。
      • 向工具提示添加偏移量,我必须这样做,否则它不会位于条形顶部,而是右移 60 像素。
      • 让 x 坐标由 recharts 处理。

      从我的代码中可以看出,我根据 recharts 文档使用了自定义工具提示

      const CustomTooltip = ({ active, payload, label }) => {
          const classes = useStyles();
          return (
              <Fragment>
                  {active && payload?.length >= 1 &&
                      <Card className={classes.toolTip}>
                          <CardContent className={classes.toolTipContent}>
                              <Typography className={classes.toolTipLabel}>{label}</Typography>
                              <Typography className={classes.toolTipValue}>{payload[0].payload.value}</Typography>
                          </CardContent>
                      </Card>
                  }
              </Fragment>
          )
      }
      

      但这只是用于格式化/样式,它也应该适用于默认设置。

      不幸的是,这似乎只在您将鼠标悬停在实际的“栏”而不是“光标”定义的整个区域时才有效。

      第二个 hacky 解决方案,适用于整个光标区域

      这让我想到了第二种解决方案,不幸的是它给出了警告

      Warning: Cannot update a component (`BarChartCard`) while rendering a different component (`label`). To locate the bad setState() call inside `label`, follow the stack trace as described in ...
      

      创建的代码在这里,任何关于如何使这项工作的评论都很棒!

      export default function BarChartCard(props) {
        const [barGraphData, setBarGraphData] = useState({});
        const [activeBarIndex, setActiveBarIndex] = useState({});
        const [toolTipYPosition, setToolTipYPosition] = useState({});
      
        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: -toolTipYPosition + 170}}
                  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
                  label={(e) => {
                    if(e.index === activeBarIndex){
                      setToolTipYPosition(e.height)
                    }
                    return null;
                  }}
                  barSize={10}
                  dataKey="value"
                  name={props.legendText}
                  fill="#EFC92B"
                  onMouseOver={(data) => {
                    setBarGraphData(data);
                  }}
                  onMouseLeave={(data) => {
                    setBarGraphData(data);
                  }}
                />
              </BarChart>
            </ResponsiveContainer>
          </BaseCard>
        );
      }
      

      【讨论】:

        猜你喜欢
        • 2019-10-03
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2018-06-17
        • 2019-07-15
        • 1970-01-01
        相关资源
        最近更新 更多