Search code examples
reactjstypescriptmaterial-uiamchartsamcharts5

Amcharts 5 Chart Doesn't Work in MUI Dialog: Could not find HTML element with id


I am using AmChart 5's XY Chart to show some data. When a button is clicked users can see this chart in a MUI Dialog to do some manipulations on it. However, when I want to show my chart in a MUI Dialog with a unique ID, it gives "Could not find HTML element with id" error. When I place the same chart outside the Dialog in the same component, everything works perfectly.

The part where I define the chart:

    useLayoutEffect(() => {
    let root = am5.Root.new(id);
    root.utc = true;

    // Set themes
    // https://www.amcharts.com/docs/v5/concepts/themes/
    root.setThemes([am5themes_Animated.new(root)]);
    

    // Create chart
    // https://www.amcharts.com/docs/v5/charts/xy-chart/
    let chart = root.container.children.push(
      am5xy.XYChart.new(root, {
        panX: false,
        panY: false,
        pinchZoomX: false,
        pinchZoomY: false,
      })
    );

    // Add cursor
    // https://www.amcharts.com/docs/v5/charts/xy-chart/cursor/
    let cursor = chart.set("cursor", am5xy.XYCursor.new(root, {}));
    cursor.lineY.set("visible", false);
    cursor.lineX.set("visible", false);

    // Create axes
    // https://www.amcharts.com/docs/v5/charts/xy-chart/axes/
    let xAxis = chart.xAxes.push(
      am5xy.DateAxis.new(root, {
        baseInterval: {
          timeUnit: "month",
          count: 3,
        },
        gridIntervals: [{ timeUnit: "month", count: 3 }],
        renderer: am5xy.AxisRendererX.new(root, {}),
        tooltip: am5.Tooltip.new(root, {}),
        dateFormats: {
          month: "MM/yy",
        },
        tooltipDateFormats: {
          month: "MM/yy",
        },
      })
    );

    let yAxis = chart.yAxes.push(
      am5xy.ValueAxis.new(root, {
        renderer: am5xy.AxisRendererY.new(root, {}),
      })
    );

    let series = chart.series.push(
      am5xy.LineSeries.new(root, {
        name: "Series",
        xAxis: xAxis,
        yAxis: yAxis,
        valueYField: "value",
        valueXField: "date",
        tooltip: am5.Tooltip.new(root, {
          labelText: "{valueY}",
        }),
        fill: am5.color("#fdce97"),
        stroke: am5.color("#fdce97"),
        width: 4,
      })
    );

    let otherSeries = chart.series.push(
      am5xy.LineSeries.new(root, {
        name: "Other Series",
        xAxis: xAxis,
        yAxis: yAxis,
        valueYField: "value",
        valueXField: "date",
        tooltip: am5.Tooltip.new(root, {
          labelText: "{valueY}",
        }),
        fill: am5.color("#19214d"),
        stroke: am5.color("#19214d"),
      })
    );

    seriesRef.current = otherSeries; // chart assigned to the reference to use later

    series.bullets.push(function () {
      let bulletCircle = am5.Circle.new(root, {
        radius: 2,
        stroke: am5.color("#fdce97"),
        strokeWidth: 1.5,
      });
      return am5.Bullet.new(root, {
        sprite: bulletCircle,
      });
    });

    otherSeries.bullets.push(function () {
      let bulletCircle = am5.Circle.new(root, {
        radius: 2,
        stroke: am5.color("#19214d"),
        strokeWidth: 1.5,
      });
      return am5.Bullet.new(root, {
        sprite: bulletCircle,
      });
    });

    // Invisible bullet which will be dragged (to avoid some conflicting between
    // drag position and bullet position which results flicker)
    otherSeries.bullets.push(() => {
      let bulletCircle = am5.Circle.new(root, {
        radius: 6,
        fillOpacity: 0,
        fill: otherSeries.get("fill"),
        draggable: true,
        cursorOverStyle: "pointer",
      });
      bulletCircle.events.on("dragged", function (e) {
        handleDrag(e);
      });
      bulletCircle.events.on("dragstop", function (e) {
        //handleChange(e, otherSeries);
      });
      return am5.Bullet.new(root, {
        sprite: bulletCircle,
      });
    });

    // Drag handler
    const handleDrag = (e: any) => {
      let point = chart.plotContainer.toLocal(e.point);
      let date = xAxis.positionToValue(xAxis.coordinateToPosition(point.x));
      let value = round(
        yAxis.positionToValue(yAxis.coordinateToPosition(point.y)),
        1
      );

      let dataItem = e.target.dataItem;
      dataItem.set("valueX", date);
      dataItem.set("valueXWorking", date);
      dataItem.set("valueY", value);
      dataItem.set("valueYWorking", value);
    };

    // Set data
    const draggableData: any = [];
    draggableValues.forEach((value: any, index: number) => {
      draggableData.push({
        value: value,
        date: dates[index],
      });
    });

    const originalData: any = [];
    originalValues.forEach((value: any, index: number) => {
      originalData.push({
        value: value,
        date: dates[index],
      });
    });

    //setOriginalData(originalData);

    series.data.processor = am5.DataProcessor.new(root, {
      numericFields: ["value"],
      dateFields: ["date"],
      dateFormat: "dd/MM/yyyy",
    });

    otherSeries.data.processor = am5.DataProcessor.new(root, {
      numericFields: ["value"],
      dateFields: ["date"],
      dateFormat: "dd/MM/yyyy",
    });

    otherSeries.data.setAll(draggableData);
    series.data.setAll(originalData);

    return () => {
      root.dispose();
    };
  }, []);

The part where I call the chart:

  return (
      <Dialog open={true} onClose={handleCloseDialog} maxWidth={dialogMaxWidth}>
        <Box
          sx={{
            display: "flex",
            flexDirection: "column",
            backgroundColor: "grey.200",
            minWidth: 500,
          }}
        >
          <Box
            sx={{
              display: "inline-flex",
              alignItems: "center",
              px: 2.5,
              py: 2.5,
            }}
          >
            <Box
              sx={{
                display: "inline-flex",
                flexDirection: "column",
                backgroundColor: "white",
                width: "100%",
                boxShadow: 1,
                borderRadius: "6px",
                p: 3,
              }}
            >
           <Box id={id} sx={{ width: "326px", height: "300px" }}></Box>
            </Box>
          </Box>
        </Box>
      </Dialog>
  );

If I were to call this function like this where the chart is outside the chart, it works perfectly:

return (
<>
      <Box id={id} sx={{ width: "326px", height: "300px" }}></Box>
      <Dialog open={true} onClose={handleCloseDialog} maxWidth={dialogMaxWidth}>
        <Box
          sx={{
            display: "flex",
            flexDirection: "column",
            backgroundColor: "grey.200",
            minWidth: 500,
          }}
        >
          <Box
            sx={{
              display: "inline-flex",
              alignItems: "center",
              px: 2.5,
              py: 2.5,
            }}
          >
            <Box
              sx={{
                display: "inline-flex",
                flexDirection: "column",
                backgroundColor: "white",
                width: "100%",
                boxShadow: 1,
                borderRadius: "6px",
                p: 3,
              }}
            >
            </Box>
          </Box>
        </Box>
      </Dialog>
    </>
  );

Solution

  • To fix this, you need to make sure that you create the amChart 5 instance only after the Dialog is open and the element is mounted. You can use a ref to access the element instead of an id, and use the onEntered prop of the Dialog component to trigger a callback function that creates the chart.

    For example:

    const chartRef = useRef(null);
    const chartInstanceRef = useRef(null);
    useEffect(() => {
      return () => {
        if (chartInstanceRef.current) {
          chartInstanceRef.current.dispose();
        }
      };
    }, []);
    
    const handleEntered = () => {
      const element = chartRef.current;
      const root = am5.Root.new(element);
    
      /* define chart */
    
      chartInstanceRef.current = root;
    };
    
    return (
      <Dialog
        open={open}
        onClose={onClose}
        onEntered={handleEntered}
        maxWidth={dialogMaxWidth}
      >
        <div ref={chartRef} style={{ width: "100%", height: "500px" }}></div>
      </Dialog>
    );