How to synchronize the position tooltip of different line charts in d3js?

Viewed 103
  1. Goal: I'm using D3.js with ReactJS I want to hover over one chart and display a tooltip on all the other charts with their own data.
  2. The actual result: The tooltip displays the data of the same hovered chart in all other charts.
  3. Code: This is what I have tried multiline-chart-tooltip-acoss-all-charts.
  4. More details: In recharts there is a syncId props if two charts have the same syncId, these two charts can sync the position tooltip.I want to sync the position tooltip with D3JS.
  5. The problem is in this file: ChartComponents/Tooltip.js.
const Tooltip = ({
    xScale,
    yScale,
    width,
    height,
    data,
    margin,
    anchorEl,
    children,
    currentIndex,
    tooltipColor,
    ...props
  }) => {
    const ref = useRef(null);
  
    const drawLine = useCallback(
      (x) => {
        d3.select(ref.current)
          .select(".tooltipLine")
          .attr("x1", x)
          .attr("x2", x)
          .attr("y1", -margin.top)
          .attr("y2", height);
      },
      [ref, height, margin]
    );
  
    const drawLineForOthers = useCallback(
      (x) => {
        d3.selectAll(".tooltip")
          .select(".tooltipLine")
          .attr("x1", x)
          .attr("x2", x)
          .attr("y1", -margin.top)
          .attr("y2", height);
      },
      [ref, height, margin]
    );
  
    const drawContent = useCallback(
      (x) => {
        const tz = Intl.DateTimeFormat().resolvedOptions().timeZone;
        // console.log('xScale.invert(x)', convertTZ(xScale.invert(x), tz));
        const tooltipContent = d3.select(ref.current).select(".tooltipContent");
        tooltipContent.attr("transform", (cur, i, nodes) => {
          const nodeWidth = nodes[i].getBoundingClientRect().width || 0;
          const translateX = nodeWidth + x > width ? x - nodeWidth - 12 : x + 8;
          return `translate(${translateX}, ${-margin.top})`;
        });
        tooltipContent
          .select(".contentTitle")
          .text("Le " + d3.timeFormat("%d %b %Y, %H:%M")(xScale.invert(x)));
        // .text(d3.timeFormat('%b %d, %Y %Z')(convertTZ(xScale.invert(x), tz)));
      },
      [xScale, margin, width]
    );
  
    const drawContentForOthers = useCallback(
      (x) => {
        // console.log('xScale.invert(x)', xScale.invert(x));
        const tooltipContentOthers = d3
          .selectAll(".tooltip")
          .select(".tooltipContent");
        tooltipContentOthers.attr("transform", (cur, i, nodes) => {
          const nodeWidth = nodes[i].getBoundingClientRect().width || 0;
          const translateX = nodeWidth + x > width ? x - nodeWidth - 12 : x + 8;
          return `translate(${translateX}, ${-margin.top})`;
        });
        tooltipContentOthers
          .select(".contentTitle")
          .text("Le " + d3.timeFormat("%d %b %Y, %H:%M")(xScale.invert(x)));
        // .text(d3.timeFormat('%b %d, %Y %H:%M')(xScale.invert(x)));
        // .text(
        //   d3.timeFormat('%b %d, %Y %H:%M')(convertTZ(xScale.invert(x) + '', tz))
        // );
      },
      [xScale, margin, width]
    );
  
    const drawBackground = useCallback(() => {
      // reset background size to defaults
      const contentBackground = d3
        .select(ref.current)
        .select(".contentBackground");
      contentBackground.attr("width", 125).attr("height", 20);
  
      // calculate new background size
      const tooltipContentElement = d3
        .select(ref.current)
        .select(".tooltipContent")
        .node();
      if (!tooltipContentElement) return;
  
      const contentSize = tooltipContentElement.getBoundingClientRect();
  
      contentBackground
        .attr("width", contentSize.width + 8)
        .attr(
          "height",
          contentSize.height > 80
            ? contentSize.height + 10
            : contentSize.height + 4
        );
    }, []);
  
    const drawBackgroundForOthers = useCallback(() => {
      // reset background size to defaults for others
      const contentBackgroundOthers = d3
        .selectAll(".tooltip")
        .select(".contentBackground")
        .filter((item, i) => currentIndex !== i);
      contentBackgroundOthers.attr("width", 125).attr("height", 40);
  
      // calculate new background size for others
      const tooltipContentElementOthers = d3
        .selectAll(".tooltip")
        .select(".tooltipContent")
        .node();
      if (!tooltipContentElementOthers) return;
  
      const contentSizeOthers = tooltipContentElementOthers.getBoundingClientRect();
      contentBackgroundOthers
        .attr("width", contentSizeOthers.width + 8)
        .attr(
          "height",
          contentSizeOthers.height > 80
            ? contentSizeOthers.height + 26
            : contentSizeOthers.height + 4
        );
    }, []);
  
    const onChangePosition = useCallback((d, i, isVisible) => {
      d3.select(ref.current)
        .selectAll(".performanceItemMarketValue")
        .filter((td, tIndex) => tIndex === i)
        .text(d && d.value && !isVisible ? "No data" : d && financial(d.value));
  
      const maxNameWidth = d3.max(
        d3.select(ref.current).selectAll(".performanceItemName").nodes(),
        (node) => node.getBoundingClientRect().width
      );
      d3.select(ref.current)
        .selectAll(".performanceItemValue")
        .attr(
          "transform",
          (datum, index, nodes) =>
            `translate(${
              nodes[index].previousSibling.getBoundingClientRect().width + 14
            },4)`
        );
  
      d3.select(ref.current)
        .selectAll(".performanceItemMarketValue")
        .attr("transform", `translate(${maxNameWidth + 60},4)`);
    }, []);
  
    const followPoints = useCallback(
      (e) => {
        const [x] = [e.layerX - margin.left];
        const xDate = xScale.invert(x);
        const bisectDate = d3.bisector((d) => d.date).left;
        let baseXPos = 0;
  
        drawCirclesOnLine();
        drawCirclesOnLineForOthers();
  
        drawLine(baseXPos);
        drawContent(baseXPos);
        drawBackground();
  
        drawLineForOthers(baseXPos);
        drawContentForOthers(baseXPos);
        drawBackgroundForOthers();
  
        function drawCirclesOnLine() {
          d3.select(ref.current)
            .selectAll(".tooltipLinePoint")
            .attr("transform", (cur, i) => {
              const index = bisectDate(data[i].items, xDate, 1);
              const d0 = data[i].items[index - 1];
              const d1 = data[i].items[index];
              let d;
              if (d1) d = xDate - d0.date > d1.date - xDate ? d1 : d0;
              if (d && d.date === undefined && d.value === undefined) {
                // move point out of container
                return "translate(-100,-100)";
              }
              const xPos = d && xScale(d.date) ? xScale(d.date) : 0;
              if (i === 0) {
                baseXPos = xPos;
              }
  
              let isVisible = true;
              if (xPos !== baseXPos) {
                isVisible = false;
              }
              const yPos = d && yScale(d.value) ? yScale(d.value) : 0;
  
              onChangePosition(d, i, isVisible);
  
              return isVisible
                ? `translate(${xPos}, ${yPos})`
                : "translate(-100,-100)";
            });
        }
  
        function drawCirclesOnLineForOthers() {
          d3.selectAll(".tooltip")
            .selectAll(".tooltipLinePoint")
            .attr("transform", (cur, i) => {
              const index = data[i] && bisectDate(data[i].items, xDate, 1);
              const d0 = data[i] && data[i].items[index - 1];
              const d1 = data[i] && data[i].items[index];
              let d = {};
              if (d1) d = xDate - d0.date > d1.date - xDate ? d1 : d0;
              if (d && d.date === undefined && d.value === undefined) {
                // move point out of container
                return "translate(-100,-100)";
              }
              const xPos = d && xScale(d.date) ? xScale(d.date) : 0;
              if (i === 0) {
                baseXPos = xPos;
              }
  
              let isVisible = true;
              if (xPos !== baseXPos) {
                isVisible = false;
              }
              const yPos = d && yScale(d.value) ? yScale(d.value) : 0;
  
              // onChangePositionForOthers(d, i, isVisible);
  
              return isVisible
                ? `translate(${xPos}, ${yPos})`
                : "translate(-100,-100)";
            });
        }
      },
      [
        // anchorEl,
        xScale,
        yScale,
        data,
        margin.left,
        drawLine,
        drawContent,
        drawBackground,
        onChangePosition,
        drawLineForOthers,
        drawContentForOthers,
        drawBackgroundForOthers
        // onChangePositionForOthers
      ]
    );
  
    useEffect(() => {
      d3.select(anchorEl)
        .on("mouseout.tooltip", () => {
          d3.select(ref.current).attr("opacity", 0);
          d3.selectAll(".tooltip").attr("opacity", 0);
        })
        .on("mouseover.tooltip", () => {
          d3.select(ref.current).attr("opacity", 1);
          d3.selectAll(".tooltip").attr("opacity", 1);
        })
        .on("mousemove.tooltip", (e) => {
          d3.select(ref.current)
            .selectAll(".tooltipLinePoint")
            .attr("opacity", 1);
          d3.selectAll(".tooltip")
            .selectAll(".tooltipLinePoint")
            .attr("opacity", 1);
          followPoints(e);
        });
    }, [anchorEl, followPoints]);
  
    if (!data.length) return null;
  
    return (
      <g ref={ref} opacity={0} {...props}>
        <line className="tooltipLine" />
        <g className="tooltipContent">
          <rect
            style={{ fill: tooltipColor }}
            className="contentBackground"
            rx={4}
            ry={4}
            opacity={0.4}
          />
          <text className="contentTitle" transform="translate(4,14)" />
          <g className="content" transform="translate(4,32)">
            {data.map(({ name, color }, i) => (
              <g key={name + "_" + i} transform={`translate(6,${22 * i})`}>
                <circle r={6} fill={color} />
                <text className="performanceItemName" transform="translate(10,4)">
                  {trimText(name, 14)}
                </text>
                <text className="performanceItemMarketValue" />
              </g>
            ))}
          </g>
        </g>
        {data.map(({ name, color }, index) => (
          <circle
            fill={color}
            className="tooltipLinePoint"
            r={6}
            key={name + "_" + index}
            opacity={0}
          />
        ))}
      </g>
    );
  }; 
0 Answers
Related